glycin-ng 0.2.2

Permissively-licensed Rust image decoder library with in-process sandboxing
docs.rs failed to build glycin-ng-0.2.2
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

glycin-ng

Crates.io Docs.rs License cargo-deny

Drop-in replacement for upstream glycin. One in-process Rust shared library.

  • ~9x smaller install. ~4 MiB vs ~37 MiB on Arch.
  • No bubblewrap. No D-Bus. No helper binaries.
  • Permissive licensing only. No LGPL or MPL transitive code.
  • Per-decode sandbox. Landlock + seccomp + rlimit on the worker thread.
                  +-----------------+
                  |  Caller thread  |
                  +--------+--------+
                           |
                           | Loader::load(bytes_or_path)
                           v
        +------------------+------------------+
        |   glycin-ng-worker thread           |
        |  +-------------------------------+  |
        |  | rlimit   (RLIMIT_AS, _CPU)    |  |
        |  +-------------------------------+  |
        |  | landlock (FS + net + scope)   |  |
        |  +-------------------------------+  |
        |  | seccomp  (BPF allowlist)      |  |
        |  +-------------------------------+  |
        |  |   Decoder  (pure Rust crate)  |  |
        |  +-------------------------------+  |
        +------------------+------------------+
                           |
                           | join, return frames + posture
                           v
                  +--------+--------+
                  |  Image, frames  |
                  +-----------------+

Quickstart

Rust

use glycin_ng::Loader;

let image = Loader::new_path("photo.png").load()?;
let frame = image.first_frame().expect("at least one frame");
let texture = frame.texture();

println!(
    "{}x{} {:?}, {} bytes",
    texture.width(),
    texture.height(),
    texture.format(),
    texture.data().len(),
);

if let glycin_ng::LandlockPosture::Enforced { abi } =
    image.sandbox_posture().landlock
{
    println!("decoded under landlock abi v{abi}");
}

Refuse degraded sandbox:

let image = Loader::new_bytes(bytes)
    .require_sandbox()
    .load()?;

require_sandbox() returns Error::SandboxUnavailable("landlock") (or "seccomp", "rlimit") on any kernel that cannot enforce a selected layer.

C

#include "glycin_ng.h"

GlycinNgLoader *loader = glycin_ng_loader_new_path("photo.png");
GlycinNgImage *image = glycin_ng_loader_load(loader);
if (!image) {
    fprintf(stderr, "%s\n", glycin_ng_last_error());
    return 1;
}

printf("%ux%u\n",
    glycin_ng_image_width(image),
    glycin_ng_image_height(image));

glycin_ng_image_free(image);

Build libglycin_ng.so plus include/glycin_ng.h:

cargo build --release --features c-api

Worked example in examples/c_load.c.

How it differs from upstream

Upstream glycin sits in the same position in the stack: it is the loader library new versions of gdk-pixbuf and GNOME apps depend on. It spawns one helper process per format under bwrap, talks to it over peer-to-peer D-Bus, and inherits LGPL / MPL transitive code from the codec libraries those helpers link against (librsvg, libjxl, libheif, libopenraw, ...).

upstream glycin glycin-ng
Install footprint ~37 MiB (glycin + librsvg + libjxl + bubblewrap; grows with glycin-loaders, libheif, libopenraw) ~4 MiB (libglycin_ng.so + shim)
Decoder license surface mixed (LGPL, MPL, BSD) permissive only (MIT, Apache, BSD, ISC, Zlib)
Decode boundary separate process per format in-process worker thread
Sandbox mechanism bwrap (mount / PID / user ns) landlock + seccomp + rlimit
IPC peer-to-peer D-Bus direct function call
Per-decode cost process spawn + namespace + IPC thread spawn + prctl
Helper binaries shipped one per format none
Behaves under Flatpak / AppImage / distrobox needs a sandbox helper to nest nests cleanly (layers only narrow further)

If you want every available codec including the LGPL ones, you want upstream glycin. If you want permissive licensing, a small install, or you're packaging into something already sandboxed where bwrap nesting is awkward, you want this.

Supported formats

Format Backing crate Decode Encode Notes
PNG / APNG png yes yes animation
JPEG jpeg-decoder yes yes
GIF gif yes yes animation
WebP image-webp yes yes animation
TIFF tiff yes yes
BMP image yes yes
ICO / CUR image yes - picks largest entry
TGA image yes -
QOI qoi yes -
OpenEXR image (exr) yes - 16 / 32-bit float, HDR-aware
PNM family image yes -
DDS image yes -
JPEG XL jxl-oxide yes -
SVG resvg / usvg yes - GTK symbolic-icon wrappers expanded

Deferred because no permissive decoder exists yet: HEIF, AVIF, RAW.

Sandbox

Each decode runs on a dedicated glycin-ng-worker thread, joined before the call returns. Three layers stack on that thread:

Layer Default What it does Failure surface
landlock on denies all FS paths to the worker; on V4+ also TCP bind/connect; on V6+ scopes abstract-unix-socket and signals Unsupported on pre-5.13 kernels
seccomp on BPF allowlist; everything else returns EPERM Unsupported if prctl fails
rlimit off RLIMIT_AS and RLIMIT_CPU from Limits PartiallyApplied per limit

Toggle layers with Loader::sandbox_selector(SandboxSelector { ... }). Inspect the result with Image::sandbox_posture() and decide whether to log, audit, or refuse a degraded posture.

Landlock negotiates up to ABI V6 at runtime and degrades cleanly. The crate ships built-in regression tests asserting both that an unlisted syscall (socket) is denied under seccomp, and that the worker spawns a rayon pool for JPEG / JXL without tripping clone3.

The dominant cost is the seccomp install: the BPF program is JIT-compiled into the kernel on every prctl(PR_SET_SECCOMP), so its overhead scales with the size of the allowlist. Landlock adds a single-digit microsecond cost on top. Run cargo bench --bench sandbox_overhead for the numbers on your specific hardware.

Limits

Every decode is bounded:

Field Default
max_width 32768
max_height 32768
max_pixels 256 Mpx
max_frames 1024
max_animation_duration 60s
decode_memory_mib 512 (RLIMIT_AS if rlimit on)
decode_cpu_seconds 30 (RLIMIT_CPU if rlimit on)

Override via Loader::limits(Limits { ... }).

Feature flags

Group Default Notes
Capability decode, metadata enable encode for PNG, JPEG, GIF, WebP, TIFF, BMP
Sandbox landlock, seccomp (Linux) toggling off is supported for portability testing, not as a production posture
Per-format png, jpeg, gif, webp, tiff, bmp, ico, tga, qoi, exr, pnm, dds, jxl, svg trim individually
ABI (off) c-api enables the cdylib build and cbindgen header

Minimum build:

cargo build --no-default-features

Trim individual formats:

cargo build --no-default-features --features decode,png,jpeg

Related crates

  • glycin-ng-libglycin-shim - libglycin-2.so.0 drop-in for systems that have hard-linked against upstream's libglycin (Arch's gdk-pixbuf2 is the canonical case).

License

MIT OR Apache-2.0.

CI runs cargo deny check on every push and PR, enforcing that no transitive dependency carries an MPL, LGPL, GPL, or other copyleft license. A failing audit is a blocker.