Skip to content
Open
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
60 changes: 60 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Security Policy

## Reporting a Vulnerability

If you discover a security vulnerability in BCC, please report it responsibly:

1. **Do not** open a public GitHub issue for security vulnerabilities.
2. Email the maintainers at the [IOVisor mailing list](https://lists.iovisor.org/g/iovisor-dev) with a description of the issue.
3. Include steps to reproduce, affected versions, and any potential mitigations.
4. Allow reasonable time for a fix before public disclosure.

## Security Advisories

### BCC-2026-001: BPF C Code Injection via CLI Arguments

**Severity:** High
**Affected versions:** All versions prior to this fix
**CVSSv3 estimate:** 7.8 (Local / High)

**Description:**
43 Python-based BCC tools accepted command-line arguments (PIDs, TIDs, UIDs, signal numbers) as unvalidated strings and interpolated them directly into BPF C source code via `bpf_text.replace()`. A local attacker with the ability to influence tool invocation (e.g., through wrapper scripts, cron jobs, or shared automation) could inject arbitrary C code into kernel-loaded BPF programs.

**Example:**
```
# Before fix: this would inject C code into the BPF program
sudo tcptop.py -p '1234; } malicious(); if (0'
```

**Fix:**
All vulnerable `argparse` arguments now use `type=int` (or a custom `positive_int_list` validator for comma-separated signal lists), ensuring non-numeric input is rejected at argument parsing time before it reaches string interpolation.

**Affected tools:** tcptop, tcpconnlat, tcplife, tcpaccept, capable, cpudist, statsnoop, filelife, filegone, compactsnoop, vfsstat, ext4dist, shmsnoop, sofdsnoop, numasched, klockstat, opensnoop, drsnoop, tcpconnect, bindsnoop, nfsslower, xfsslower, zfsslower, ext4slower, btrfsslower, f2fsslower, execsnoop, killsnoop, ttysnoop, and 14 tools in `tools/old/`.

---

### BCC-2026-002: World-Writable Directory Permissions

**Severity:** Medium
**Affected versions:** All versions prior to this fix
**CVSSv3 estimate:** 5.5 (Local / Medium)

**Description:**
`src/cc/bpf_module.cc` created the BPF program tag cache directory (`/var/tmp/bcc/`) and its subdirectories with mode `0777` (world-writable). A local attacker could:

1. Create symlinks in the world-writable directory pointing to sensitive files.
2. When BCC writes cached BPF program source files, it would follow symlinks and overwrite arbitrary files owned by root.

**Fix:**
- Directory creation now uses mode `0700` (owner-only access).
- All `open()` calls include `O_NOFOLLOW` to refuse to follow symlinks.
- All `write()` return values are now checked for errors.

---

## Secure Usage Guidelines

1. **Always run BCC tools directly** — avoid passing user-controlled input to BCC tool arguments without validation.
2. **Restrict access** — BCC tools require root or `CAP_BPF`/`CAP_SYS_ADMIN`. Limit who can execute them.
3. **Keep BCC updated** — apply security patches promptly.
4. **Audit wrapper scripts** — if you wrap BCC tools in scripts that accept external input, validate all numeric arguments before passing them through.
25 changes: 17 additions & 8 deletions src/cc/bpf_module.cc
Original file line number Diff line number Diff line change
Expand Up @@ -693,53 +693,62 @@ int BPFModule::annotate_prog_tag(const string &name, int prog_fd,
return -1;
}

err = mkdir(BCC_PROG_TAG_DIR, 0777);
err = mkdir(BCC_PROG_TAG_DIR, 0700);
if (err && errno != EEXIST) {
fprintf(stderr, "cannot create " BCC_PROG_TAG_DIR "\n");
return -1;
}

char buf[128];
::snprintf(buf, sizeof(buf), BCC_PROG_TAG_DIR "/bpf_prog_%llx", tag1);
err = mkdir(buf, 0777);
err = mkdir(buf, 0700);
if (err && errno != EEXIST) {
fprintf(stderr, "cannot create %s\n", buf);
return -1;
}

::snprintf(buf, sizeof(buf), BCC_PROG_TAG_DIR "/bpf_prog_%llx/%s.c",
tag1, name.data());
FileDesc fd(open(buf, O_CREAT | O_WRONLY | O_TRUNC, 0644));
FileDesc fd(open(buf, O_CREAT | O_WRONLY | O_TRUNC | O_NOFOLLOW, 0644));
if (fd < 0) {
fprintf(stderr, "cannot create %s\n", buf);
return -1;
}

const char *src = function_source(name);
write(fd, src, strlen(src));
if (write(fd, src, strlen(src)) < 0) {
fprintf(stderr, "cannot write to %s\n", buf);
return -1;
}

::snprintf(buf, sizeof(buf), BCC_PROG_TAG_DIR "/bpf_prog_%llx/%s.rewritten.c",
tag1, name.data());
fd = open(buf, O_CREAT | O_WRONLY | O_TRUNC, 0644);
fd = open(buf, O_CREAT | O_WRONLY | O_TRUNC | O_NOFOLLOW, 0644);
if (fd < 0) {
fprintf(stderr, "cannot create %s\n", buf);
return -1;
}

src = function_source_rewritten(name);
write(fd, src, strlen(src));
if (write(fd, src, strlen(src)) < 0) {
fprintf(stderr, "cannot write to %s\n", buf);
return -1;
}

if (!src_dbg_fmap_[name].empty()) {
::snprintf(buf, sizeof(buf), BCC_PROG_TAG_DIR "/bpf_prog_%llx/%s.dis.txt",
tag1, name.data());
fd = open(buf, O_CREAT | O_WRONLY | O_TRUNC, 0644);
fd = open(buf, O_CREAT | O_WRONLY | O_TRUNC | O_NOFOLLOW, 0644);
if (fd < 0) {
fprintf(stderr, "cannot create %s\n", buf);
return -1;
}

const char *src = src_dbg_fmap_[name].c_str();
write(fd, src, strlen(src));
if (write(fd, src, strlen(src)) < 0) {
fprintf(stderr, "cannot write to %s\n", buf);
return -1;
}
}

return 0;
Expand Down
29 changes: 29 additions & 0 deletions src/python/bcc/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import ctypes as ct
import sys
import traceback
Expand All @@ -19,6 +20,34 @@

from .libbcc import lib

def positive_int(value):
"""argparse type: accept integers >= 0, reject non-numeric strings."""
ival = int(value)
if ival < 0:
raise argparse.ArgumentTypeError(
"%s is not a non-negative integer" % value)
return ival

def positive_nonzero_int(value):
"""argparse type: accept integers >= 1, reject zero and non-numeric strings."""
ival = int(value)
if ival <= 0:
raise argparse.ArgumentTypeError(
"%s is not a positive integer" % value)
return ival

def positive_int_list(value):
"""argparse type: accept comma-separated list of non-negative integers."""
result = []
for item in value.split(','):
item = item.strip()
ival = int(item)
if ival < 0:
raise argparse.ArgumentTypeError(
"%s is not a non-negative integer" % item)
result.append(ival)
return result

def _read_cpu_range(path):
cpus = []
with open(path, 'r') as f:
Expand Down
Loading