Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions .clusterfuzzlite/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FROM gcr.io/oss-fuzz-base/base-builder-rust@sha256:a960440a8543df2411ca489eff995366ce8041b086141cc5092664d793f20e9e
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

COPY . $SRC/fast-mlsirm
WORKDIR $SRC/fast-mlsirm
5 changes: 5 additions & 0 deletions .clusterfuzzlite/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash -eux

cd "$SRC/fast-mlsirm"
cargo fuzz build -O neg_loglik
cp fuzz/target/x86_64-unknown-linux-gnu/release/neg_loglik "$OUT/"
1 change: 1 addition & 0 deletions .clusterfuzzlite/project.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
language: rust
18 changes: 18 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
- package-ecosystem: pip
directory: /
schedule:
interval: weekly
- package-ecosystem: cargo
directory: /
schedule:
interval: weekly
- package-ecosystem: cargo
directory: /fuzz
schedule:
interval: weekly
44 changes: 44 additions & 0 deletions .github/workflows/cflite_pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: ClusterFuzzLite PR fuzzing

on:
pull_request:
paths:
- ".clusterfuzzlite/**"
- ".github/workflows/cflite_pr.yml"
- "Cargo.toml"
- "crates/**"
- "fuzz/**"

permissions:
contents: read

jobs:
PR:
runs-on: ubuntu-24.04
permissions:
contents: read
security-events: write
concurrency:
group: ${{ github.workflow }}-${{ matrix.sanitizer }}-${{ github.ref }}
cancel-in-progress: true
strategy:
fail-fast: false
matrix:
sanitizer: [address]
steps:
- name: Build fuzzers (${{ matrix.sanitizer }})
id: build
uses: google/clusterfuzzlite/actions/build_fuzzers@82652fb49e77bc29c35da1167bb286e93c6bcc05
with:
language: rust
github-token: ${{ secrets.GITHUB_TOKEN }}
sanitizer: ${{ matrix.sanitizer }}
- name: Run fuzzers (${{ matrix.sanitizer }})
id: run
uses: google/clusterfuzzlite/actions/run_fuzzers@82652fb49e77bc29c35da1167bb286e93c6bcc05
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
fuzz-seconds: 120
mode: code-change
output-sarif: true
sanitizer: ${{ matrix.sanitizer }}
18 changes: 11 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,24 @@ on:
branches: [main, develop]
pull_request:

permissions:
contents: read

jobs:
python:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: "3.12"
python-version: "3.12.3"
- run: python -m pip install -e .[dev]
- run: pytest

rust:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
- run: rustup toolchain install 1.97.0 --profile minimal
- run: rustup default 1.97.0
- run: cargo test
26 changes: 26 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: CodeQL

on:
push:
branches: [main, develop]
pull_request:
schedule:
- cron: "24 3 * * 2"

permissions:
contents: read

jobs:
analyze:
name: Analyze Python
runs-on: ubuntu-24.04
permissions:
actions: read
contents: read
security-events: write
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
- uses: github/codeql-action/init@641a925cfafe92d0fdf8b239ba4053e3f8d99d6d
with:
languages: python
- uses: github/codeql-action/analyze@641a925cfafe92d0fdf8b239ba4053e3f8d99d6d
21 changes: 21 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Security Policy

## Supported Versions

Security fixes are prepared for the default branch and the latest released
source package when one exists.

## Reporting a Vulnerability

Please report suspected vulnerabilities through GitHub private vulnerability
reporting:

https://github.com/ContextualWisdomLab/fast-mlsirm/security/advisories/new

Do not open a public issue for a vulnerability report. Include the affected
version or commit, reproduction steps, expected impact, and any known safe
workaround.

We aim to acknowledge vulnerability reports within 7 days. After validation, we
will coordinate a disclosure timeline and target a fix or mitigation plan within
90 days, depending on severity and affected users.
109 changes: 105 additions & 4 deletions crates/mlsirm-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ pub fn neg_loglik_and_grad(
let a = alpha.exp();
let mut dist2 = config.eps_distance;
for k in 0..config.latent_dim {
let diff = params.xi[p * config.latent_dim + k] - params.zeta[i * config.latent_dim + k];
let diff =
params.xi[p * config.latent_dim + k] - params.zeta[i * config.latent_dim + k];
dist2 += diff * diff;
}
let r = if uses_space { dist2.sqrt() } else { 0.0 };
Expand All @@ -120,8 +121,8 @@ pub fn neg_loglik_and_grad(
if uses_space {
grad.tau += e * (-gamma * r);
for k in 0..config.latent_dim {
let diff =
params.xi[p * config.latent_dim + k] - params.zeta[i * config.latent_dim + k];
let diff = params.xi[p * config.latent_dim + k]
- params.zeta[i * config.latent_dim + k];
let common = gamma * diff / r;
grad.xi[p * config.latent_dim + k] += e * (-common);
grad.zeta[i * config.latent_dim + k] += e * common;
Expand All @@ -148,7 +149,12 @@ fn add_penalty(
value += add_l2(&params.theta, penalty.lambda_theta, 0.0, &mut grad.theta);
value += add_l2(&params.b, penalty.lambda_b, 0.0, &mut grad.b);
if free_alpha {
value += add_l2(&params.alpha, penalty.lambda_alpha, penalty.mu_alpha, &mut grad.alpha);
value += add_l2(
&params.alpha,
penalty.lambda_alpha,
penalty.mu_alpha,
&mut grad.alpha,
);
}
if uses_space {
value += add_l2(&params.xi, penalty.lambda_xi, 0.0, &mut grad.xi);
Expand Down Expand Up @@ -261,4 +267,99 @@ mod tests {
let finite_diff = (obj_plus - base) / h;
assert!((finite_diff - grad.tau).abs() < 1e-5);
}

#[test]
fn mask_excludes_observations_from_likelihood_and_gradients() {
let cfg = config();
let p = params();
let penalty = PenaltyConfig {
lambda_theta: 0.0,
lambda_xi: 0.0,
lambda_zeta: 0.0,
lambda_b: 0.0,
lambda_alpha: 0.0,
lambda_tau: 0.0,
mu_alpha: 0.0,
mu_tau: 0.0,
};
let y = vec![1.0, 0.0, 0.0, 1.0];
let mask = vec![true, false, false, true];

let (masked_obj, masked_grad, masked_loglik) =
neg_loglik_and_grad(&y, Some(&mask), &[0, 0], &p, &cfg, &penalty);

let first_cfg = ModelConfig {
n_persons: 1,
n_items: 1,
n_dims: 1,
latent_dim: 2,
model_type: ModelType::Mls2plm,
eps_distance: cfg.eps_distance,
};
let first_params = Params {
theta: vec![p.theta[0]],
alpha: vec![p.alpha[0]],
b: vec![p.b[0]],
xi: vec![p.xi[0], p.xi[1]],
zeta: vec![p.zeta[0], p.zeta[1]],
tau: p.tau,
};
let (first_obj, _, _) =
neg_loglik_and_grad(&[1.0], None, &[0], &first_params, &first_cfg, &penalty);

let second_params = Params {
theta: vec![p.theta[1]],
alpha: vec![p.alpha[1]],
b: vec![p.b[1]],
xi: vec![p.xi[2], p.xi[3]],
zeta: vec![p.zeta[2], p.zeta[3]],
tau: p.tau,
};
let (second_obj, _, _) =
neg_loglik_and_grad(&[1.0], None, &[0], &second_params, &first_cfg, &penalty);

assert!((masked_obj - (first_obj + second_obj)).abs() < 1e-12);
assert!((masked_loglik + masked_obj).abs() < 1e-12);
assert_eq!(masked_grad.theta.len(), cfg.n_persons * cfg.n_dims);
}

#[test]
fn mirt_omits_latent_space_and_tau_penalty_terms() {
let cfg = ModelConfig {
n_persons: 1,
n_items: 1,
n_dims: 1,
latent_dim: 2,
model_type: ModelType::Mirt,
eps_distance: 1e-8,
};
let p = Params {
theta: vec![0.5],
alpha: vec![0.2],
b: vec![-0.1],
xi: vec![10.0, -10.0],
zeta: vec![-7.0, 3.0],
tau: 4.0,
};
let penalty = PenaltyConfig {
lambda_theta: 0.0,
lambda_xi: 0.0,
lambda_zeta: 0.0,
lambda_b: 0.0,
lambda_alpha: 0.0,
lambda_tau: 10.0,
mu_alpha: 0.0,
mu_tau: -2.0,
};

let (got, grad, loglik) = neg_loglik_and_grad(&[1.0], None, &[0], &p, &cfg, &penalty);
let eta = 0.2_f64.exp() * 0.5 - 0.1;
let expected = softplus(eta) - eta;

assert!((got - expected).abs() < 1e-12);
assert!((loglik + expected).abs() < 1e-12);
assert_eq!(grad.xi, vec![0.0, 0.0]);
assert_eq!(grad.zeta, vec![0.0, 0.0]);
assert_eq!(grad.tau, 0.0);
}
}
94 changes: 94 additions & 0 deletions fuzz/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading