pybinaryguard 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pybinaryguard/__init__.py +78 -0
- pybinaryguard/__main__.py +7 -0
- pybinaryguard/_compat/__init__.py +0 -0
- pybinaryguard/agent/__init__.py +63 -0
- pybinaryguard/agent/guard.py +232 -0
- pybinaryguard/agent/recommender.py +283 -0
- pybinaryguard/agent/schema.py +200 -0
- pybinaryguard/agent/simulator.py +430 -0
- pybinaryguard/agent/tool_interface.py +474 -0
- pybinaryguard/analyzers/__init__.py +209 -0
- pybinaryguard/analyzers/base.py +40 -0
- pybinaryguard/analyzers/dependency_analyzer.py +336 -0
- pybinaryguard/analyzers/elf_analyzer.py +754 -0
- pybinaryguard/analyzers/symbol_analyzer.py +280 -0
- pybinaryguard/analyzers/wheel_analyzer.py +308 -0
- pybinaryguard/cli/__init__.py +5 -0
- pybinaryguard/cli/commands.py +414 -0
- pybinaryguard/cli/formatters.py +720 -0
- pybinaryguard/cli/main.py +250 -0
- pybinaryguard/diagnostics/__init__.py +13 -0
- pybinaryguard/diagnostics/explainer.py +356 -0
- pybinaryguard/diagnostics/findings.py +146 -0
- pybinaryguard/diagnostics/suggestions.py +508 -0
- pybinaryguard/frameworks/__init__.py +20 -0
- pybinaryguard/frameworks/onnxruntime.py +214 -0
- pybinaryguard/frameworks/pytorch.py +223 -0
- pybinaryguard/frameworks/tensorflow.py +266 -0
- pybinaryguard/frameworks/tensorrt.py +189 -0
- pybinaryguard/models/__init__.py +19 -0
- pybinaryguard/models/enums.py +102 -0
- pybinaryguard/models/finding.py +109 -0
- pybinaryguard/models/package.py +121 -0
- pybinaryguard/models/system.py +118 -0
- pybinaryguard/plugins/__init__.py +19 -0
- pybinaryguard/plugins/contrib/__init__.py +7 -0
- pybinaryguard/plugins/contrib/gstreamer.py +109 -0
- pybinaryguard/plugins/contrib/jetson.py +385 -0
- pybinaryguard/plugins/contrib/opencv.py +306 -0
- pybinaryguard/plugins/contrib/tensorrt.py +426 -0
- pybinaryguard/plugins/hooks.py +273 -0
- pybinaryguard/plugins/loader.py +190 -0
- pybinaryguard/predictor/__init__.py +23 -0
- pybinaryguard/predictor/dependency_graph.py +226 -0
- pybinaryguard/predictor/linker_simulator.py +161 -0
- pybinaryguard/predictor/predictor.py +144 -0
- pybinaryguard/predictor/resolver.py +282 -0
- pybinaryguard/probes/__init__.py +73 -0
- pybinaryguard/probes/base.py +45 -0
- pybinaryguard/probes/board_probe.py +248 -0
- pybinaryguard/probes/cpu_probe.py +176 -0
- pybinaryguard/probes/glibc_probe.py +215 -0
- pybinaryguard/probes/gpu_probe.py +430 -0
- pybinaryguard/probes/library_probe.py +132 -0
- pybinaryguard/probes/os_probe.py +227 -0
- pybinaryguard/probes/python_probe.py +135 -0
- pybinaryguard/probes/toolchain_probe.py +89 -0
- pybinaryguard/probes/venv_probe.py +111 -0
- pybinaryguard/profiles/__init__.py +12 -0
- pybinaryguard/profiles/engine.py +343 -0
- pybinaryguard/rules/__init__.py +24 -0
- pybinaryguard/rules/base.py +57 -0
- pybinaryguard/rules/builtin/__init__.py +136 -0
- pybinaryguard/rules/builtin/arch_rules.py +86 -0
- pybinaryguard/rules/builtin/board_profile_rules.py +282 -0
- pybinaryguard/rules/builtin/container_rules.py +170 -0
- pybinaryguard/rules/builtin/cpu_rules.py +204 -0
- pybinaryguard/rules/builtin/cuda_rules.py +760 -0
- pybinaryguard/rules/builtin/dependency_rules.py +278 -0
- pybinaryguard/rules/builtin/framework_rules.py +252 -0
- pybinaryguard/rules/builtin/glibc_rules.py +320 -0
- pybinaryguard/rules/builtin/numpy_rules.py +110 -0
- pybinaryguard/rules/builtin/predictive_rules.py +165 -0
- pybinaryguard/rules/builtin/python_abi_rules.py +259 -0
- pybinaryguard/rules/builtin/source_build_rules.py +150 -0
- pybinaryguard/rules/builtin/venv_rules.py +159 -0
- pybinaryguard/rules/engine.py +123 -0
- pybinaryguard/scanner.py +904 -0
- pybinaryguard/scoring/__init__.py +19 -0
- pybinaryguard/scoring/engine.py +416 -0
- pybinaryguard/snapshot/__init__.py +20 -0
- pybinaryguard/snapshot/generator.py +168 -0
- pybinaryguard/snapshot/lockfile.py +152 -0
- pybinaryguard/snapshot/verifier.py +315 -0
- pybinaryguard/validators/__init__.py +7 -0
- pybinaryguard/validators/import_validator.py +231 -0
- pybinaryguard-1.0.0.dist-info/METADATA +876 -0
- pybinaryguard-1.0.0.dist-info/RECORD +91 -0
- pybinaryguard-1.0.0.dist-info/WHEEL +5 -0
- pybinaryguard-1.0.0.dist-info/entry_points.txt +8 -0
- pybinaryguard-1.0.0.dist-info/licenses/LICENSE +21 -0
- pybinaryguard-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Board profile-aware rules for embedded device intelligence.
|
|
2
|
+
|
|
3
|
+
These rules leverage board profiles to provide targeted diagnostics for
|
|
4
|
+
specific embedded hardware platforms.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import List
|
|
10
|
+
|
|
11
|
+
from pybinaryguard.models.enums import Severity
|
|
12
|
+
from pybinaryguard.models.finding import Finding
|
|
13
|
+
from pybinaryguard.models.package import PackageBinaryInfo
|
|
14
|
+
from pybinaryguard.models.system import SystemProfile
|
|
15
|
+
from pybinaryguard.profiles import match_board_profile
|
|
16
|
+
from pybinaryguard.rules.base import Rule
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class KNOWN_BROKEN_WHEEL(Rule):
|
|
20
|
+
"""Detects packages known to be broken on the detected board."""
|
|
21
|
+
|
|
22
|
+
rule_id = "KNOWN_BROKEN_WHEEL"
|
|
23
|
+
description = "Checks packages against board-specific broken wheel database"
|
|
24
|
+
|
|
25
|
+
def is_applicable(self, profile: SystemProfile) -> bool:
|
|
26
|
+
board_profile = match_board_profile(profile)
|
|
27
|
+
return board_profile is not None
|
|
28
|
+
|
|
29
|
+
def evaluate(
|
|
30
|
+
self, profile: SystemProfile, packages: List[PackageBinaryInfo]
|
|
31
|
+
) -> List[Finding]:
|
|
32
|
+
findings: List[Finding] = []
|
|
33
|
+
board_profile = match_board_profile(profile)
|
|
34
|
+
if board_profile is None:
|
|
35
|
+
return findings
|
|
36
|
+
|
|
37
|
+
for package in packages:
|
|
38
|
+
if not package.has_binaries:
|
|
39
|
+
continue
|
|
40
|
+
package_version = package.package_version or None
|
|
41
|
+
broken = board_profile.is_package_known_broken(
|
|
42
|
+
package.package_name, package_version
|
|
43
|
+
)
|
|
44
|
+
if broken:
|
|
45
|
+
findings.append(
|
|
46
|
+
Finding(
|
|
47
|
+
rule_id=self.rule_id,
|
|
48
|
+
severity=Severity.CRITICAL,
|
|
49
|
+
title=(
|
|
50
|
+
f"{package.package_name} is known broken on "
|
|
51
|
+
f"{board_profile.display_name}"
|
|
52
|
+
),
|
|
53
|
+
explanation=(
|
|
54
|
+
f"Package {package.package_name} version "
|
|
55
|
+
f"{broken.versions} is incompatible with "
|
|
56
|
+
f"{board_profile.display_name}. "
|
|
57
|
+
f"Reason: {broken.reason}"
|
|
58
|
+
),
|
|
59
|
+
package=package.package_name,
|
|
60
|
+
suggestion=broken.recommendation,
|
|
61
|
+
confidence=1.0,
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
return findings
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class BOARD_INCOMPATIBLE_PACKAGE(Rule):
|
|
68
|
+
"""Detects packages fundamentally incompatible with the board."""
|
|
69
|
+
|
|
70
|
+
rule_id = "BOARD_INCOMPATIBLE_PACKAGE"
|
|
71
|
+
description = "Checks for packages that cannot work on detected hardware"
|
|
72
|
+
|
|
73
|
+
def is_applicable(self, profile: SystemProfile) -> bool:
|
|
74
|
+
board_profile = match_board_profile(profile)
|
|
75
|
+
return board_profile is not None
|
|
76
|
+
|
|
77
|
+
def evaluate(
|
|
78
|
+
self, profile: SystemProfile, packages: List[PackageBinaryInfo]
|
|
79
|
+
) -> List[Finding]:
|
|
80
|
+
findings: List[Finding] = []
|
|
81
|
+
board_profile = match_board_profile(profile)
|
|
82
|
+
if board_profile is None:
|
|
83
|
+
return findings
|
|
84
|
+
|
|
85
|
+
for package in packages:
|
|
86
|
+
if board_profile.is_package_incompatible(package.package_name):
|
|
87
|
+
findings.append(
|
|
88
|
+
Finding(
|
|
89
|
+
rule_id=self.rule_id,
|
|
90
|
+
severity=Severity.CRITICAL,
|
|
91
|
+
title=(
|
|
92
|
+
f"{package.package_name} is incompatible with "
|
|
93
|
+
f"{board_profile.display_name}"
|
|
94
|
+
),
|
|
95
|
+
explanation=(
|
|
96
|
+
f"Package {package.package_name} requires hardware "
|
|
97
|
+
f"features not available on {board_profile.display_name}."
|
|
98
|
+
),
|
|
99
|
+
package=package.package_name,
|
|
100
|
+
suggestion=(
|
|
101
|
+
f"Remove {package.package_name} or use a board "
|
|
102
|
+
f"with required hardware features"
|
|
103
|
+
),
|
|
104
|
+
confidence=1.0,
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
return findings
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class BOARD_CUDA_VERSION_MISMATCH(Rule):
|
|
111
|
+
"""Detects CUDA version exceeding board maximum."""
|
|
112
|
+
|
|
113
|
+
rule_id = "BOARD_CUDA_VERSION_MISMATCH"
|
|
114
|
+
description = "Checks if package requires CUDA version beyond board limit"
|
|
115
|
+
|
|
116
|
+
def is_applicable(self, profile: SystemProfile) -> bool:
|
|
117
|
+
board_profile = match_board_profile(profile)
|
|
118
|
+
return (
|
|
119
|
+
board_profile is not None
|
|
120
|
+
and board_profile.max_cuda_version is not None
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def evaluate(
|
|
124
|
+
self, profile: SystemProfile, packages: List[PackageBinaryInfo]
|
|
125
|
+
) -> List[Finding]:
|
|
126
|
+
findings: List[Finding] = []
|
|
127
|
+
board_profile = match_board_profile(profile)
|
|
128
|
+
if board_profile is None or board_profile.max_cuda_version is None:
|
|
129
|
+
return findings
|
|
130
|
+
|
|
131
|
+
for package in packages:
|
|
132
|
+
if not package.cuda_version:
|
|
133
|
+
continue
|
|
134
|
+
try:
|
|
135
|
+
pkg_major = int(package.cuda_version.split(".")[0])
|
|
136
|
+
board_max_major = int(
|
|
137
|
+
board_profile.max_cuda_version.split(".")[0]
|
|
138
|
+
)
|
|
139
|
+
if pkg_major > board_max_major:
|
|
140
|
+
findings.append(
|
|
141
|
+
Finding(
|
|
142
|
+
rule_id=self.rule_id,
|
|
143
|
+
severity=Severity.CRITICAL,
|
|
144
|
+
title=(
|
|
145
|
+
f"{package.package_name} requires CUDA "
|
|
146
|
+
f"{package.cuda_version}, but "
|
|
147
|
+
f"{board_profile.display_name} max is "
|
|
148
|
+
f"{board_profile.max_cuda_version}"
|
|
149
|
+
),
|
|
150
|
+
explanation=(
|
|
151
|
+
f"Package {package.package_name} requires "
|
|
152
|
+
f"CUDA {package.cuda_version}, but "
|
|
153
|
+
f"{board_profile.display_name} only supports "
|
|
154
|
+
f"up to CUDA {board_profile.max_cuda_version}."
|
|
155
|
+
),
|
|
156
|
+
package=package.package_name,
|
|
157
|
+
suggestion=(
|
|
158
|
+
f"Install a version of {package.package_name} "
|
|
159
|
+
f"compatible with CUDA "
|
|
160
|
+
f"{board_profile.max_cuda_version}"
|
|
161
|
+
),
|
|
162
|
+
confidence=0.95,
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
except (ValueError, IndexError):
|
|
166
|
+
pass
|
|
167
|
+
return findings
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class BOARD_GLIBC_MISMATCH(Rule):
|
|
171
|
+
"""Detects GLIBC mismatch against board recommendations."""
|
|
172
|
+
|
|
173
|
+
rule_id = "BOARD_GLIBC_MISMATCH"
|
|
174
|
+
description = "Checks if system GLIBC differs from board recommendation"
|
|
175
|
+
|
|
176
|
+
def is_applicable(self, profile: SystemProfile) -> bool:
|
|
177
|
+
board_profile = match_board_profile(profile)
|
|
178
|
+
return (
|
|
179
|
+
board_profile is not None
|
|
180
|
+
and board_profile.recommended_glibc is not None
|
|
181
|
+
and profile.glibc_version is not None
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def evaluate(
|
|
185
|
+
self, profile: SystemProfile, packages: List[PackageBinaryInfo]
|
|
186
|
+
) -> List[Finding]:
|
|
187
|
+
findings: List[Finding] = []
|
|
188
|
+
board_profile = match_board_profile(profile)
|
|
189
|
+
if board_profile is None or board_profile.recommended_glibc is None:
|
|
190
|
+
return findings
|
|
191
|
+
|
|
192
|
+
system_glibc = profile.glibc_version
|
|
193
|
+
if system_glibc is None:
|
|
194
|
+
return findings
|
|
195
|
+
|
|
196
|
+
recommended = board_profile.recommended_glibc
|
|
197
|
+
try:
|
|
198
|
+
rec_parts = tuple(int(x) for x in recommended.split(".")[:2])
|
|
199
|
+
if system_glibc != rec_parts:
|
|
200
|
+
findings.append(
|
|
201
|
+
Finding(
|
|
202
|
+
rule_id=self.rule_id,
|
|
203
|
+
severity=Severity.WARNING,
|
|
204
|
+
title=(
|
|
205
|
+
f"GLIBC {system_glibc[0]}.{system_glibc[1]} "
|
|
206
|
+
f"differs from {board_profile.display_name} "
|
|
207
|
+
f"recommended {recommended}"
|
|
208
|
+
),
|
|
209
|
+
explanation=(
|
|
210
|
+
f"Your system has GLIBC "
|
|
211
|
+
f"{system_glibc[0]}.{system_glibc[1]}, but "
|
|
212
|
+
f"{board_profile.display_name} is tested with "
|
|
213
|
+
f"GLIBC {recommended}."
|
|
214
|
+
),
|
|
215
|
+
suggestion=(
|
|
216
|
+
f"Consider using "
|
|
217
|
+
f"{board_profile.recommended_os or 'recommended OS'} "
|
|
218
|
+
f"which provides GLIBC {recommended}"
|
|
219
|
+
),
|
|
220
|
+
confidence=0.7,
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
except (ValueError, IndexError):
|
|
224
|
+
pass
|
|
225
|
+
return findings
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class BOARD_PYTHON_VERSION_UNSUPPORTED(Rule):
|
|
229
|
+
"""Detects Python version not validated for the board."""
|
|
230
|
+
|
|
231
|
+
rule_id = "BOARD_PYTHON_VERSION_UNSUPPORTED"
|
|
232
|
+
description = "Checks if Python version is in board's validated list"
|
|
233
|
+
|
|
234
|
+
def is_applicable(self, profile: SystemProfile) -> bool:
|
|
235
|
+
board_profile = match_board_profile(profile)
|
|
236
|
+
return (
|
|
237
|
+
board_profile is not None
|
|
238
|
+
and len(board_profile.python_versions) > 0
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def evaluate(
|
|
242
|
+
self, profile: SystemProfile, packages: List[PackageBinaryInfo]
|
|
243
|
+
) -> List[Finding]:
|
|
244
|
+
findings: List[Finding] = []
|
|
245
|
+
board_profile = match_board_profile(profile)
|
|
246
|
+
if board_profile is None or not board_profile.python_versions:
|
|
247
|
+
return findings
|
|
248
|
+
|
|
249
|
+
py_version = (
|
|
250
|
+
f"{profile.python_version[0]}.{profile.python_version[1]}"
|
|
251
|
+
)
|
|
252
|
+
if py_version not in board_profile.python_versions:
|
|
253
|
+
findings.append(
|
|
254
|
+
Finding(
|
|
255
|
+
rule_id=self.rule_id,
|
|
256
|
+
severity=Severity.WARNING,
|
|
257
|
+
title=(
|
|
258
|
+
f"Python {py_version} not validated for "
|
|
259
|
+
f"{board_profile.display_name}"
|
|
260
|
+
),
|
|
261
|
+
explanation=(
|
|
262
|
+
f"Python {py_version} has not been validated for "
|
|
263
|
+
f"{board_profile.display_name}. Validated: "
|
|
264
|
+
f"{', '.join(board_profile.python_versions)}"
|
|
265
|
+
),
|
|
266
|
+
suggestion=(
|
|
267
|
+
f"Use Python {board_profile.python_versions[0]} "
|
|
268
|
+
f"for best compatibility"
|
|
269
|
+
),
|
|
270
|
+
confidence=0.6,
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
return findings
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
__all__ = [
|
|
277
|
+
"KNOWN_BROKEN_WHEEL",
|
|
278
|
+
"BOARD_INCOMPATIBLE_PACKAGE",
|
|
279
|
+
"BOARD_CUDA_VERSION_MISMATCH",
|
|
280
|
+
"BOARD_GLIBC_MISMATCH",
|
|
281
|
+
"BOARD_PYTHON_VERSION_UNSUPPORTED",
|
|
282
|
+
]
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Container environment compatibility rules.
|
|
2
|
+
|
|
3
|
+
These rules detect GPU-related misconfiguration when running inside a
|
|
4
|
+
container (Docker, Podman, etc.). Containers do not automatically get
|
|
5
|
+
access to the host GPU unless explicitly configured.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import List, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
from pybinaryguard.models.enums import Severity
|
|
13
|
+
from pybinaryguard.models.finding import Finding
|
|
14
|
+
from pybinaryguard.models.package import PackageBinaryInfo
|
|
15
|
+
from pybinaryguard.models.system import SystemProfile
|
|
16
|
+
from pybinaryguard.rules.base import Rule
|
|
17
|
+
from pybinaryguard.rules.builtin.cuda_rules import (
|
|
18
|
+
_driver_major,
|
|
19
|
+
_is_cuda_package,
|
|
20
|
+
_max_cuda_for_driver,
|
|
21
|
+
_fmt_ver,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ContainerNoGPUMountRule(Rule):
|
|
26
|
+
"""Warns when a GPU package runs inside a container without GPU access.
|
|
27
|
+
|
|
28
|
+
Running ``docker run`` without ``--gpus all`` (or the NVIDIA Container
|
|
29
|
+
Toolkit) means the GPU is invisible to the container. The package
|
|
30
|
+
will silently fall back to CPU or raise an error.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
rule_id = "CONTAINER_NO_GPU_MOUNT"
|
|
34
|
+
description = (
|
|
35
|
+
"Warn when GPU packages run inside a container that lacks GPU "
|
|
36
|
+
"device mounts."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def is_applicable(self, profile: SystemProfile) -> bool:
|
|
40
|
+
return profile.is_container and not profile.gpu_available
|
|
41
|
+
|
|
42
|
+
def evaluate(
|
|
43
|
+
self,
|
|
44
|
+
profile: SystemProfile,
|
|
45
|
+
packages: List[PackageBinaryInfo],
|
|
46
|
+
) -> List[Finding]:
|
|
47
|
+
findings: List[Finding] = []
|
|
48
|
+
for pkg in packages:
|
|
49
|
+
if not _is_cuda_package(pkg):
|
|
50
|
+
continue
|
|
51
|
+
findings.append(
|
|
52
|
+
Finding(
|
|
53
|
+
rule_id=self.rule_id,
|
|
54
|
+
severity=Severity.WARNING,
|
|
55
|
+
title=(
|
|
56
|
+
f"{pkg.package_name} needs a GPU but none is "
|
|
57
|
+
f"visible in this container"
|
|
58
|
+
),
|
|
59
|
+
explanation=(
|
|
60
|
+
f"You are running inside a container "
|
|
61
|
+
f"({profile.container_runtime.value}) and package "
|
|
62
|
+
f"{pkg.package_name} {pkg.package_version} is a "
|
|
63
|
+
f"GPU-enabled build, but no GPU device was "
|
|
64
|
+
f"detected inside this container. This usually "
|
|
65
|
+
f"means the container was started without GPU "
|
|
66
|
+
f"passthrough."
|
|
67
|
+
),
|
|
68
|
+
technical_detail=(
|
|
69
|
+
f"Container runtime: "
|
|
70
|
+
f"{profile.container_runtime.value}, "
|
|
71
|
+
f"GPU visible: False"
|
|
72
|
+
),
|
|
73
|
+
suggestion=(
|
|
74
|
+
f"Run the container with GPU access:\n"
|
|
75
|
+
f" docker run --gpus all ... "
|
|
76
|
+
f"# Docker with NVIDIA Container Toolkit\n"
|
|
77
|
+
f" podman run --device nvidia.com/gpu=all ... "
|
|
78
|
+
f"# Podman with CDI\n\n"
|
|
79
|
+
f"Make sure the NVIDIA Container Toolkit is "
|
|
80
|
+
f"installed on the host:\n"
|
|
81
|
+
f" sudo apt install nvidia-container-toolkit "
|
|
82
|
+
f"# Debian/Ubuntu\n"
|
|
83
|
+
f" sudo systemctl restart docker"
|
|
84
|
+
),
|
|
85
|
+
package=pkg.package_name,
|
|
86
|
+
package_version=pkg.package_version,
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
return findings
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ContainerDriverMismatchRule(Rule):
|
|
93
|
+
"""Warns when the container's CUDA may exceed the host driver capability.
|
|
94
|
+
|
|
95
|
+
Inside a container the CUDA toolkit version can be newer than what
|
|
96
|
+
the host GPU driver supports. The container shares the host's
|
|
97
|
+
kernel-mode driver, so if the driver is too old, CUDA calls will
|
|
98
|
+
fail.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
rule_id = "CONTAINER_DRIVER_MISMATCH"
|
|
102
|
+
description = (
|
|
103
|
+
"Check that the container's CUDA version does not exceed the "
|
|
104
|
+
"host driver's capability."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def is_applicable(self, profile: SystemProfile) -> bool:
|
|
108
|
+
return (
|
|
109
|
+
profile.is_container
|
|
110
|
+
and profile.gpu_available
|
|
111
|
+
and profile.gpu_driver_version is not None
|
|
112
|
+
and profile.cuda_runtime_version is not None
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def evaluate(
|
|
116
|
+
self,
|
|
117
|
+
profile: SystemProfile,
|
|
118
|
+
packages: List[PackageBinaryInfo],
|
|
119
|
+
) -> List[Finding]:
|
|
120
|
+
findings: List[Finding] = []
|
|
121
|
+
if (
|
|
122
|
+
profile.gpu_driver_version is None
|
|
123
|
+
or profile.cuda_runtime_version is None
|
|
124
|
+
):
|
|
125
|
+
return findings
|
|
126
|
+
|
|
127
|
+
drv_major = _driver_major(profile.gpu_driver_version)
|
|
128
|
+
if drv_major is None:
|
|
129
|
+
return findings
|
|
130
|
+
|
|
131
|
+
max_cuda = _max_cuda_for_driver(drv_major)
|
|
132
|
+
if max_cuda is None:
|
|
133
|
+
return findings
|
|
134
|
+
|
|
135
|
+
cuda_rt = profile.cuda_runtime_version
|
|
136
|
+
if cuda_rt <= max_cuda:
|
|
137
|
+
return findings
|
|
138
|
+
|
|
139
|
+
findings.append(
|
|
140
|
+
Finding(
|
|
141
|
+
rule_id=self.rule_id,
|
|
142
|
+
severity=Severity.WARNING,
|
|
143
|
+
title=(
|
|
144
|
+
"Container CUDA version exceeds host driver capability"
|
|
145
|
+
),
|
|
146
|
+
explanation=(
|
|
147
|
+
f"This container has CUDA {_fmt_ver(cuda_rt)} "
|
|
148
|
+
f"installed, but the host's NVIDIA driver "
|
|
149
|
+
f"({profile.gpu_driver_version}) only supports CUDA "
|
|
150
|
+
f"up to {_fmt_ver(max_cuda)}. Because the container "
|
|
151
|
+
f"shares the host's kernel-mode GPU driver, CUDA "
|
|
152
|
+
f"calls will fail even though the CUDA toolkit is "
|
|
153
|
+
f"installed inside the container."
|
|
154
|
+
),
|
|
155
|
+
technical_detail=(
|
|
156
|
+
f"Container CUDA: {_fmt_ver(cuda_rt)}, "
|
|
157
|
+
f"Host driver: {profile.gpu_driver_version}, "
|
|
158
|
+
f"Max CUDA for driver: {_fmt_ver(max_cuda)}"
|
|
159
|
+
),
|
|
160
|
+
suggestion=(
|
|
161
|
+
f"Option 1 -- upgrade the GPU driver on the host to "
|
|
162
|
+
f"one that supports CUDA {_fmt_ver(cuda_rt)}.\n\n"
|
|
163
|
+
f"Option 2 -- use a container image with CUDA <= "
|
|
164
|
+
f"{_fmt_ver(max_cuda)}:\n"
|
|
165
|
+
f" docker run --gpus all nvidia/cuda:"
|
|
166
|
+
f"{_fmt_ver(max_cuda)}-runtime-ubuntu22.04"
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
return findings
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""CPU instruction-set rules.
|
|
2
|
+
|
|
3
|
+
These rules detect packages that require advanced CPU instruction sets
|
|
4
|
+
(AVX, AVX2, AVX-512, etc.) which may not be available on the host. Running
|
|
5
|
+
such a package on an unsupported CPU triggers a ``SIGILL``
|
|
6
|
+
(illegal instruction) signal, causing the process to crash instantly without
|
|
7
|
+
a meaningful Python traceback.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Dict, List, Set
|
|
13
|
+
|
|
14
|
+
from pybinaryguard.models.enums import Severity
|
|
15
|
+
from pybinaryguard.models.finding import Finding
|
|
16
|
+
from pybinaryguard.models.package import PackageBinaryInfo
|
|
17
|
+
from pybinaryguard.models.system import SystemProfile
|
|
18
|
+
from pybinaryguard.rules.base import Rule
|
|
19
|
+
|
|
20
|
+
# Mapping of well-known packages to the instruction sets they are known
|
|
21
|
+
# to require. An empty set means the package *may* use advanced
|
|
22
|
+
# instructions but does not always require them (build-dependent).
|
|
23
|
+
KNOWN_INSTRUCTION_REQUIREMENTS: Dict[str, Set[str]] = {
|
|
24
|
+
"tensorflow": {"avx"},
|
|
25
|
+
"tensorflow-cpu": {"avx"},
|
|
26
|
+
"tensorflow-gpu": {"avx"},
|
|
27
|
+
"numpy": set(), # modern numpy may need AVX2 on some builds
|
|
28
|
+
"faiss-cpu": {"avx2"},
|
|
29
|
+
"faiss-gpu": {"avx2"},
|
|
30
|
+
"pytorch": set(),
|
|
31
|
+
"torch": set(),
|
|
32
|
+
"scipy": set(),
|
|
33
|
+
"scikit-learn": set(),
|
|
34
|
+
"xgboost": set(),
|
|
35
|
+
"lightgbm": set(),
|
|
36
|
+
"onnxruntime": {"avx"},
|
|
37
|
+
"onnxruntime-gpu": {"avx"},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_INSTRUCTION_FRIENDLY_NAMES: Dict[str, str] = {
|
|
41
|
+
"avx": "AVX (Advanced Vector Extensions)",
|
|
42
|
+
"avx2": "AVX2 (Advanced Vector Extensions 2)",
|
|
43
|
+
"avx512": "AVX-512",
|
|
44
|
+
"sse42": "SSE 4.2",
|
|
45
|
+
"neon": "ARM NEON",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _system_has_instruction(profile: SystemProfile, instruction: str) -> bool:
|
|
50
|
+
"""Check whether the system supports the given instruction set."""
|
|
51
|
+
mapping: Dict[str, bool] = {
|
|
52
|
+
"avx": profile.has_avx,
|
|
53
|
+
"avx2": profile.has_avx2,
|
|
54
|
+
"avx512": profile.has_avx512,
|
|
55
|
+
"sse42": profile.has_sse42,
|
|
56
|
+
"neon": profile.has_neon,
|
|
57
|
+
}
|
|
58
|
+
return mapping.get(instruction, False)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AVX2RequiredRule(Rule):
|
|
62
|
+
"""Detects packages that require AVX2 on a CPU without it.
|
|
63
|
+
|
|
64
|
+
AVX2 is commonly used in high-performance numerical libraries to
|
|
65
|
+
speed up vector and matrix operations. Older or low-power CPUs
|
|
66
|
+
(virtual machines on older hosts, Atom processors, etc.) may lack
|
|
67
|
+
AVX2 support.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
rule_id = "AVX2_REQUIRED"
|
|
71
|
+
description = (
|
|
72
|
+
"Flag packages that require AVX2 when the host CPU lacks it."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def is_applicable(self, profile: SystemProfile) -> bool:
|
|
76
|
+
"""Skip if the system already has AVX2."""
|
|
77
|
+
return not profile.has_avx2
|
|
78
|
+
|
|
79
|
+
def evaluate(
|
|
80
|
+
self,
|
|
81
|
+
profile: SystemProfile,
|
|
82
|
+
packages: List[PackageBinaryInfo],
|
|
83
|
+
) -> List[Finding]:
|
|
84
|
+
findings: List[Finding] = []
|
|
85
|
+
for pkg in packages:
|
|
86
|
+
pkg_lower = pkg.package_name.lower()
|
|
87
|
+
requirements = KNOWN_INSTRUCTION_REQUIREMENTS.get(pkg_lower)
|
|
88
|
+
if requirements is None:
|
|
89
|
+
continue
|
|
90
|
+
if "avx2" not in requirements:
|
|
91
|
+
continue
|
|
92
|
+
findings.append(
|
|
93
|
+
Finding(
|
|
94
|
+
rule_id=self.rule_id,
|
|
95
|
+
severity=Severity.CRITICAL,
|
|
96
|
+
title=(
|
|
97
|
+
f"{pkg.package_name} requires AVX2 instructions"
|
|
98
|
+
),
|
|
99
|
+
explanation=(
|
|
100
|
+
f"Package {pkg.package_name} "
|
|
101
|
+
f"{pkg.package_version} is known to require "
|
|
102
|
+
f"AVX2 (Advanced Vector Extensions 2) CPU "
|
|
103
|
+
f"instructions, but your CPU ({profile.cpu_model or 'unknown'}) "
|
|
104
|
+
f"does not support AVX2. When the package tries "
|
|
105
|
+
f"to execute an AVX2 instruction your process "
|
|
106
|
+
f"will be killed by an 'Illegal instruction' "
|
|
107
|
+
f"signal (SIGILL) with no Python traceback."
|
|
108
|
+
),
|
|
109
|
+
technical_detail=(
|
|
110
|
+
f"CPU model: {profile.cpu_model or 'unknown'}, "
|
|
111
|
+
f"has_avx2: False"
|
|
112
|
+
),
|
|
113
|
+
suggestion=(
|
|
114
|
+
f"Option 1 -- look for a build that does not "
|
|
115
|
+
f"require AVX2 (some packages publish 'noavx' "
|
|
116
|
+
f"variants).\n\n"
|
|
117
|
+
f"Option 2 -- use conda-forge, which often provides "
|
|
118
|
+
f"builds for older CPUs:\n"
|
|
119
|
+
f" conda install -c conda-forge "
|
|
120
|
+
f"{pkg.package_name}\n\n"
|
|
121
|
+
f"Option 3 -- build from source with AVX2 "
|
|
122
|
+
f"disabled (if the package supports it)."
|
|
123
|
+
),
|
|
124
|
+
package=pkg.package_name,
|
|
125
|
+
package_version=pkg.package_version,
|
|
126
|
+
confidence=0.9,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
return findings
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class IllegalInstructionRiskRule(Rule):
|
|
133
|
+
"""General check for packages that may trigger SIGILL.
|
|
134
|
+
|
|
135
|
+
Consults the ``KNOWN_INSTRUCTION_REQUIREMENTS`` table to determine
|
|
136
|
+
whether the host CPU provides all instruction sets a package needs.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
rule_id = "ILLEGAL_INSTRUCTION_RISK"
|
|
140
|
+
description = (
|
|
141
|
+
"Detect packages whose instruction-set requirements may not "
|
|
142
|
+
"be met by the host CPU."
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def evaluate(
|
|
146
|
+
self,
|
|
147
|
+
profile: SystemProfile,
|
|
148
|
+
packages: List[PackageBinaryInfo],
|
|
149
|
+
) -> List[Finding]:
|
|
150
|
+
findings: List[Finding] = []
|
|
151
|
+
for pkg in packages:
|
|
152
|
+
pkg_lower = pkg.package_name.lower()
|
|
153
|
+
requirements = KNOWN_INSTRUCTION_REQUIREMENTS.get(pkg_lower)
|
|
154
|
+
if requirements is None:
|
|
155
|
+
continue
|
|
156
|
+
missing: List[str] = []
|
|
157
|
+
for instr in sorted(requirements):
|
|
158
|
+
if not _system_has_instruction(profile, instr):
|
|
159
|
+
missing.append(instr)
|
|
160
|
+
if not missing:
|
|
161
|
+
continue
|
|
162
|
+
friendly = ", ".join(
|
|
163
|
+
_INSTRUCTION_FRIENDLY_NAMES.get(m, m.upper()) for m in missing
|
|
164
|
+
)
|
|
165
|
+
findings.append(
|
|
166
|
+
Finding(
|
|
167
|
+
rule_id=self.rule_id,
|
|
168
|
+
severity=Severity.CRITICAL,
|
|
169
|
+
title=(
|
|
170
|
+
f"{pkg.package_name} may crash with "
|
|
171
|
+
f"'Illegal instruction'"
|
|
172
|
+
),
|
|
173
|
+
explanation=(
|
|
174
|
+
f"Package {pkg.package_name} "
|
|
175
|
+
f"{pkg.package_version} is known to require "
|
|
176
|
+
f"the following CPU instruction set(s): "
|
|
177
|
+
f"{friendly}. Your CPU "
|
|
178
|
+
f"({profile.cpu_model or 'unknown'}) does not "
|
|
179
|
+
f"support {'them' if len(missing) > 1 else 'it'}. "
|
|
180
|
+
f"This will cause the process to be killed with "
|
|
181
|
+
f"a SIGILL (Illegal instruction) signal the "
|
|
182
|
+
f"moment the package tries to use the missing "
|
|
183
|
+
f"instruction."
|
|
184
|
+
),
|
|
185
|
+
technical_detail=(
|
|
186
|
+
f"Missing instructions: {', '.join(missing)}, "
|
|
187
|
+
f"CPU model: {profile.cpu_model or 'unknown'}, "
|
|
188
|
+
f"CPU flags: {', '.join(sorted(profile.cpu_flags)) if profile.cpu_flags else 'unknown'}"
|
|
189
|
+
),
|
|
190
|
+
suggestion=(
|
|
191
|
+
f"Option 1 -- install an alternative build that "
|
|
192
|
+
f"does not require {friendly}.\n\n"
|
|
193
|
+
f"Option 2 -- use conda-forge:\n"
|
|
194
|
+
f" conda install -c conda-forge "
|
|
195
|
+
f"{pkg.package_name}\n\n"
|
|
196
|
+
f"Option 3 -- run inside a VM or container on "
|
|
197
|
+
f"hardware that supports these instructions."
|
|
198
|
+
),
|
|
199
|
+
package=pkg.package_name,
|
|
200
|
+
package_version=pkg.package_version,
|
|
201
|
+
confidence=0.85,
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
return findings
|