Skip to content

Commit d093516

Browse files
authored
Merge pull request #110 from pythonbpf/fix/default-gpl-license
Add default GPL license if not specified in the program
2 parents de259c8 + 47586a4 commit d093516

9 files changed

Lines changed: 118 additions & 24 deletions

File tree

‎docs/getting-started/quickstart.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,10 @@ def LICENSE() -> str:
9696
return "GPL"
9797
```
9898

99-
* The Linux kernel requires BPF programs to declare a license
99+
* The Linux kernel reads a license from every BPF program
100100
* Most kernel features require GPL-compatible licenses
101101
* This is defined as a BPF global variable
102+
* If you leave it out, PythonBPF uses `"GPL"` and logs a warning saying so
102103

103104
### Compilation and Execution
104105

@@ -197,7 +198,7 @@ def xdp_pass(ctx: c_void_p) -> c_int64:
197198

198199
## Best Practices
199200

200-
1. **Always include a LICENSE** - Required by the kernel
201+
1. **Declare a LICENSE** - Without one, PythonBPF defaults to `"GPL"` and warns
201202
2. **Use type hints** - Required by PythonBPF to generate correct code
202203
3. **Return the correct type** - Match the expected return type for your program type
203204
4. **Test incrementally** - Start simple and add complexity gradually

‎docs/user-guide/decorators.md‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -281,9 +281,10 @@ BPF global variables are values that:
281281

282282
### Common Global Variables
283283

284-
#### LICENSE (Required)
284+
#### LICENSE
285285

286-
Every BPF program must declare a license:
286+
Every BPF program carries a license, which the kernel reads when the program is
287+
loaded. Declare it as a string global:
287288

288289
```python
289290
@bpf
@@ -292,14 +293,24 @@ def LICENSE() -> str:
292293
return "GPL"
293294
```
294295

295-
Valid licenses include:
296+
If a program defines no `LICENSE`, PythonBPF uses `"GPL"`, the kernel's own
297+
license, and logs a warning:
298+
299+
```text
300+
No LICENSE defined; defaulting to "GPL". Define @bpf @bpfglobal def LICENSE() -> str to choose another license.
301+
```
302+
303+
Define `LICENSE` yourself to choose a different license or to silence the
304+
warning. The kernel treats these as GPL-compatible:
296305
* `"GPL"` - GNU General Public License
297306
* `"GPL v2"` - GPL version 2
307+
* `"GPL and additional rights"`
298308
* `"Dual BSD/GPL"` - Dual licensed
299309
* `"Dual MIT/GPL"` - Dual licensed
310+
* `"Dual MPL/GPL"` - Dual licensed
300311

301312
```{warning}
302-
Many BPF features require a GPL-compatible license. Using a non-GPL license may prevent your program from loading or accessing certain kernel features.
313+
Many BPF features require a GPL-compatible license. A program with any other license may not call GPL-only helpers, including `bpf_trace_printk`, which `print()` uses; the verifier rejects it with "cannot call GPL-restricted function from non-GPL compatible program".
303314
```
304315

305316
#### Custom Global Variables

‎docs/user-guide/index.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def my_function(ctx: c_void_p) -> c_int64:
6969
# BPF logic here
7070
return 0
7171

72-
# License (required)
72+
# License (optional: without one, PythonBPF uses "GPL" and logs a warning)
7373
@bpf
7474
@bpfglobal
7575
def LICENSE() -> str:

‎pythonbpf/license_pass.py‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55

66
logger: Logger = logging.getLogger(__name__)
77

8+
# The license a program gets when it declares none. The kernel is GPL, and a
9+
# program whose license is not GPL-compatible may not call GPL-only helpers
10+
# (bpf_trace_printk, which `print` lowers to, is one).
11+
DEFAULT_LICENSE = "GPL"
12+
813

914
def emit_license(module: ir.Module, license_str: str):
1015
license_bytes = license_str.encode("utf8") + b"\x00"
@@ -24,7 +29,10 @@ def emit_license(module: ir.Module, license_str: str):
2429

2530

2631
def license_processing(tree, compilation_context):
27-
"""Process the LICENSE function decorated with @bpf and @bpfglobal and return the section name"""
32+
"""Process the LICENSE function decorated with @bpf and @bpfglobal and return the section name.
33+
34+
A program without one gets DEFAULT_LICENSE, with a warning.
35+
"""
2836
count = 0
2937
for node in tree.body:
3038
if isinstance(node, ast.FunctionDef) and node.name == "LICENSE":
@@ -52,4 +60,10 @@ def license_processing(tree, compilation_context):
5260
)
5361
else:
5462
raise SyntaxError("ERROR: Multiple LICENSE globals defined")
55-
return None
63+
64+
logger.warning(
65+
f'No LICENSE defined; defaulting to "{DEFAULT_LICENSE}". Define '
66+
"@bpf @bpfglobal def LICENSE() -> str to choose another license."
67+
)
68+
emit_license(compilation_context.module, DEFAULT_LICENSE)
69+
return "LICENSE"

‎tests/failing_tests/license.py‎

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# A license that is not GPL-compatible may not call GPL-only helpers: `print`
2+
# lowers to bpf_trace_printk, so the verifier rejects this program. The
3+
# counterpart of passing_tests/license_default.py, which proves the default
4+
# license is GPL.
5+
6+
from pythonbpf import compile, bpf, bpfglobal, section
7+
from ctypes import c_void_p, c_int64
8+
9+
10+
@bpf
11+
@section("tracepoint/syscalls/sys_enter_getpid")
12+
def sometag(ctx: c_void_p) -> c_int64:
13+
print("hello")
14+
return c_int64(0)
15+
16+
17+
@bpf
18+
@bpfglobal
19+
def LICENSE() -> str:
20+
return "Proprietary"
21+
22+
23+
compile()
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# No LICENSE: the program gets "GPL", with a warning. `print` lowers to
2+
# bpf_trace_printk, which is GPL-only, so the verifier only accepts this
3+
# program if the default license reaches the kernel.
4+
5+
from pythonbpf import compile, bpf, section
6+
from ctypes import c_void_p, c_int64
7+
8+
9+
@bpf
10+
@section("tracepoint/syscalls/sys_enter_getpid")
11+
def sometag(ctx: c_void_p) -> c_int64:
12+
a = 1 + 2
13+
print(f"{a}")
14+
return c_int64(0)
15+
16+
17+
compile()

‎tests/test_config.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
"failing_tests/return_struct.py" = {reason = "Returning a struct value from an integer function is a type error (nothing to dereference to an integer)", level = "ir"}
2121

22-
"failing_tests/license.py" = {reason = "Missing LICENSE global produces IR that llc rejects — should be caught earlier with a clear error message", level = "llc"}
22+
"failing_tests/license_non_gpl.py" = {reason = "Negative fixture: a non-GPL license may not call the GPL-only bpf_trace_printk that print lowers to", level = "verifier", match = "cannot call GPL-restricted function from non-GPL compatible program"}
2323

2424
"failing_tests/undeclared_values.py" = {reason = "Undeclared variable used in f-string — should raise SyntaxError (correct behaviour, test documents it)", level = "ir"}
2525

‎tests/test_license_ir.py‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""
2+
The license section: a program without LICENSE gets "GPL", with a warning,
3+
and an explicit LICENSE is emitted as written, with no warning.
4+
"""
5+
6+
import logging
7+
from pathlib import Path
8+
9+
from tests.framework.compiler import run_ir_generation
10+
11+
TESTS_DIR = Path(__file__).parent
12+
13+
14+
def _license_global(text):
15+
"""The LICENSE global as compile_to_ir emits it: NUL-terminated bytes."""
16+
data = text.encode() + b"\x00"
17+
elems = ", ".join(f"i8 {b}" for b in data)
18+
return (
19+
f'@"LICENSE" = dso_local global [{len(data)} x i8] [{elems}], section "license"'
20+
)
21+
22+
23+
def _license_ir(name, tmp_path, caplog):
24+
ll_path = tmp_path / "output.ll"
25+
with caplog.at_level(logging.WARNING, logger="pythonbpf.license_pass"):
26+
run_ir_generation(TESTS_DIR / name, ll_path)
27+
warnings = [
28+
r.getMessage() for r in caplog.records if r.name == "pythonbpf.license_pass"
29+
]
30+
return ll_path.read_text(), warnings
31+
32+
33+
def test_missing_license_defaults_to_gpl(tmp_path, caplog):
34+
ir, warnings = _license_ir("passing_tests/license_default.py", tmp_path, caplog)
35+
assert _license_global("GPL") in ir
36+
assert any('defaulting to "GPL"' in w for w in warnings), warnings
37+
38+
39+
def test_explicit_license_is_kept(tmp_path, caplog):
40+
ir, warnings = _license_ir("failing_tests/license_non_gpl.py", tmp_path, caplog)
41+
assert _license_global("Proprietary") in ir
42+
assert not warnings, warnings

0 commit comments

Comments
 (0)