From b843b8f343a98bc96dd2fa3d3657bd87e5b7efed Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Fri, 9 Oct 2026 03:01:11 +0530 Subject: [PATCH 1/3] Core: Default to a GPL license, with a warning, when none is defined Without a LICENSE, @llvm.compiler.used still named one, and opt stopped on "use of undefined value @LICENSE". A program with no LICENSE now gets "GPL", the kernel's own license, and a warning that says so; an explicit LICENSE is emitted as written. The old failing license test becomes passing_tests/license_default.py: it calls print, which lowers to the GPL-only bpf_trace_printk, so the verifier accepts it only if the default reaches the kernel. license_non_gpl.py is the counterpart, a verifier-level negative fixture pinned to the kernel's GPL rejection, and test_license_ir.py checks both license sections and the warning. Co-Authored-By: Claude Opus 5.5 (1M context) --- pythonbpf/license_pass.py | 18 +++++++++-- tests/failing_tests/license.py | 14 --------- tests/failing_tests/license_non_gpl.py | 23 ++++++++++++++ tests/passing_tests/license_default.py | 17 +++++++++++ tests/test_config.toml | 2 +- tests/test_license_ir.py | 42 ++++++++++++++++++++++++++ 6 files changed, 99 insertions(+), 17 deletions(-) delete mode 100644 tests/failing_tests/license.py create mode 100644 tests/failing_tests/license_non_gpl.py create mode 100644 tests/passing_tests/license_default.py create mode 100644 tests/test_license_ir.py 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 From 4d2bf6842cba6008b4f1ccf83bdc32ad5c27b303 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Fri, 9 Oct 2026 03:01:13 +0530 Subject: [PATCH 2/3] Docs: Document the GPL default for programs without a LICENSE Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/getting-started/quickstart.md | 5 +++-- docs/user-guide/decorators.md | 19 +++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) 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 From 47586a49ade382cff8961d21b03d544c54b13448 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Fri, 9 Oct 2026 04:26:27 +0530 Subject: [PATCH 3/3] Docs: Stop calling LICENSE required in the user guide overview The overview example still labelled the LICENSE declaration as required, which contradicts the GPL default documented in decorators.md and the quickstart. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/user-guide/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: