Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
19 changes: 15 additions & 4 deletions docs/user-guide/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Comment on lines +296 to +297

```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
Expand Down
2 changes: 1 addition & 1 deletion docs/user-guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
18 changes: 16 additions & 2 deletions pythonbpf/license_pass.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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":
Expand Down Expand Up @@ -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"
14 changes: 0 additions & 14 deletions tests/failing_tests/license.py

This file was deleted.

23 changes: 23 additions & 0 deletions tests/failing_tests/license_non_gpl.py
Original file line number Diff line number Diff line change
@@ -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()
17 changes: 17 additions & 0 deletions tests/passing_tests/license_default.py
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 1 addition & 1 deletion tests/test_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"}

Expand Down
42 changes: 42 additions & 0 deletions tests/test_license_ir.py
Original file line number Diff line number Diff line change
@@ -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
Loading