diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index b463478c..c5030121 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -96,9 +96,10 @@ def LICENSE() -> str: return "GPL" ``` -* The Linux kernel requires BPF programs to declare a license +* The Linux kernel reads a license from every BPF program * Most kernel features require GPL-compatible licenses * This is defined as a BPF global variable +* If you leave it out, PythonBPF uses `"GPL"` and logs a warning saying so ### Compilation and Execution @@ -197,7 +198,7 @@ def xdp_pass(ctx: c_void_p) -> c_int64: ## Best Practices -1. **Always include a LICENSE** - Required by the kernel +1. **Declare a LICENSE** - Without one, PythonBPF defaults to `"GPL"` and warns 2. **Use type hints** - Required by PythonBPF to generate correct code 3. **Return the correct type** - Match the expected return type for your program type 4. **Test incrementally** - Start simple and add complexity gradually diff --git a/docs/user-guide/decorators.md b/docs/user-guide/decorators.md index c884fc6c..c541cdad 100644 --- a/docs/user-guide/decorators.md +++ b/docs/user-guide/decorators.md @@ -281,9 +281,10 @@ BPF global variables are values that: ### Common Global Variables -#### LICENSE (Required) +#### LICENSE -Every BPF program must declare a license: +Every BPF program carries a license, which the kernel reads when the program is +loaded. Declare it as a string global: ```python @bpf @@ -292,14 +293,24 @@ def LICENSE() -> str: return "GPL" ``` -Valid licenses include: +If a program defines no `LICENSE`, PythonBPF uses `"GPL"`, the kernel's own +license, and logs a warning: + +```text +No LICENSE defined; defaulting to "GPL". Define @bpf @bpfglobal def LICENSE() -> str to choose another license. +``` + +Define `LICENSE` yourself to choose a different license or to silence the +warning. The kernel treats these as GPL-compatible: * `"GPL"` - GNU General Public License * `"GPL v2"` - GPL version 2 +* `"GPL and additional rights"` * `"Dual BSD/GPL"` - Dual licensed * `"Dual MIT/GPL"` - Dual licensed +* `"Dual MPL/GPL"` - Dual licensed ```{warning} -Many BPF features require a GPL-compatible license. Using a non-GPL license may prevent your program from loading or accessing certain kernel features. +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". ``` #### Custom Global Variables diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md index a7504f93..07d6a4df 100644 --- a/docs/user-guide/index.md +++ b/docs/user-guide/index.md @@ -69,7 +69,7 @@ def my_function(ctx: c_void_p) -> c_int64: # BPF logic here return 0 -# License (required) +# License (optional: without one, PythonBPF uses "GPL" and logs a warning) @bpf @bpfglobal def LICENSE() -> str: diff --git a/pythonbpf/license_pass.py b/pythonbpf/license_pass.py index 425b1979..64e799c8 100644 --- a/pythonbpf/license_pass.py +++ b/pythonbpf/license_pass.py @@ -5,6 +5,11 @@ logger: Logger = logging.getLogger(__name__) +# The license a program gets when it declares none. The kernel is GPL, and a +# program whose license is not GPL-compatible may not call GPL-only helpers +# (bpf_trace_printk, which `print` lowers to, is one). +DEFAULT_LICENSE = "GPL" + def emit_license(module: ir.Module, license_str: str): license_bytes = license_str.encode("utf8") + b"\x00" @@ -24,7 +29,10 @@ def emit_license(module: ir.Module, license_str: str): def license_processing(tree, compilation_context): - """Process the LICENSE function decorated with @bpf and @bpfglobal and return the section name""" + """Process the LICENSE function decorated with @bpf and @bpfglobal and return the section name. + + A program without one gets DEFAULT_LICENSE, with a warning. + """ count = 0 for node in tree.body: if isinstance(node, ast.FunctionDef) and node.name == "LICENSE": @@ -52,4 +60,10 @@ def license_processing(tree, compilation_context): ) else: raise SyntaxError("ERROR: Multiple LICENSE globals defined") - return None + + logger.warning( + f'No LICENSE defined; defaulting to "{DEFAULT_LICENSE}". Define ' + "@bpf @bpfglobal def LICENSE() -> str to choose another license." + ) + emit_license(compilation_context.module, DEFAULT_LICENSE) + return "LICENSE" diff --git a/tests/failing_tests/license.py b/tests/failing_tests/license.py deleted file mode 100644 index 9853ab72..00000000 --- a/tests/failing_tests/license.py +++ /dev/null @@ -1,14 +0,0 @@ -from pythonbpf import compile, bpf, section -from ctypes import c_void_p, c_int64 - - -# FAILS WHEN THERE IS NO LICENSE. which is wrong. -@bpf -@section("sometag1") -def sometag(ctx: c_void_p) -> c_int64: - a = 1 + 2 - print(f"{a}") - return c_int64(0) - - -compile() diff --git a/tests/failing_tests/license_non_gpl.py b/tests/failing_tests/license_non_gpl.py new file mode 100644 index 00000000..0ec84455 --- /dev/null +++ b/tests/failing_tests/license_non_gpl.py @@ -0,0 +1,23 @@ +# A license that is not GPL-compatible may not call GPL-only helpers: `print` +# lowers to bpf_trace_printk, so the verifier rejects this program. The +# counterpart of passing_tests/license_default.py, which proves the default +# license is GPL. + +from pythonbpf import compile, bpf, bpfglobal, section +from ctypes import c_void_p, c_int64 + + +@bpf +@section("tracepoint/syscalls/sys_enter_getpid") +def sometag(ctx: c_void_p) -> c_int64: + print("hello") + return c_int64(0) + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "Proprietary" + + +compile() diff --git a/tests/passing_tests/license_default.py b/tests/passing_tests/license_default.py new file mode 100644 index 00000000..d6b2e554 --- /dev/null +++ b/tests/passing_tests/license_default.py @@ -0,0 +1,17 @@ +# No LICENSE: the program gets "GPL", with a warning. `print` lowers to +# bpf_trace_printk, which is GPL-only, so the verifier only accepts this +# program if the default license reaches the kernel. + +from pythonbpf import compile, bpf, section +from ctypes import c_void_p, c_int64 + + +@bpf +@section("tracepoint/syscalls/sys_enter_getpid") +def sometag(ctx: c_void_p) -> c_int64: + a = 1 + 2 + print(f"{a}") + return c_int64(0) + + +compile() diff --git a/tests/test_config.toml b/tests/test_config.toml index 453650f2..7e67d0ca 100644 --- a/tests/test_config.toml +++ b/tests/test_config.toml @@ -19,7 +19,7 @@ "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"} -"failing_tests/license.py" = {reason = "Missing LICENSE global produces IR that llc rejects — should be caught earlier with a clear error message", level = "llc"} +"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"} "failing_tests/undeclared_values.py" = {reason = "Undeclared variable used in f-string — should raise SyntaxError (correct behaviour, test documents it)", level = "ir"} diff --git a/tests/test_license_ir.py b/tests/test_license_ir.py new file mode 100644 index 00000000..bff5eacd --- /dev/null +++ b/tests/test_license_ir.py @@ -0,0 +1,42 @@ +""" +The license section: a program without LICENSE gets "GPL", with a warning, +and an explicit LICENSE is emitted as written, with no warning. +""" + +import logging +from pathlib import Path + +from tests.framework.compiler import run_ir_generation + +TESTS_DIR = Path(__file__).parent + + +def _license_global(text): + """The LICENSE global as compile_to_ir emits it: NUL-terminated bytes.""" + data = text.encode() + b"\x00" + elems = ", ".join(f"i8 {b}" for b in data) + return ( + f'@"LICENSE" = dso_local global [{len(data)} x i8] [{elems}], section "license"' + ) + + +def _license_ir(name, tmp_path, caplog): + ll_path = tmp_path / "output.ll" + with caplog.at_level(logging.WARNING, logger="pythonbpf.license_pass"): + run_ir_generation(TESTS_DIR / name, ll_path) + warnings = [ + r.getMessage() for r in caplog.records if r.name == "pythonbpf.license_pass" + ] + return ll_path.read_text(), warnings + + +def test_missing_license_defaults_to_gpl(tmp_path, caplog): + ir, warnings = _license_ir("passing_tests/license_default.py", tmp_path, caplog) + assert _license_global("GPL") in ir + assert any('defaulting to "GPL"' in w for w in warnings), warnings + + +def test_explicit_license_is_kept(tmp_path, caplog): + ir, warnings = _license_ir("failing_tests/license_non_gpl.py", tmp_path, caplog) + assert _license_global("Proprietary") in ir + assert not warnings, warnings