glycin-ng

May 22, 2026 ยท View on GitHub

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 glycinglycin-ng
Install footprint~37 MiB (glycin + librsvg + libjxl + bubblewrap; grows with glycin-loaders, libheif, libopenraw)~4 MiB (libglycin_ng.so + shim)
Decoder license surfacemixed (LGPL, MPL, BSD)permissive only (MIT, Apache, BSD, ISC, Zlib)
Decode boundaryseparate process per formatin-process worker thread
Sandbox mechanismbwrap (mount / PID / user ns)landlock + seccomp + rlimit
IPCpeer-to-peer D-Busdirect function call
Per-decode costprocess spawn + namespace + IPCthread spawn + prctl
Helper binaries shippedone per formatnone
Behaves under Flatpak / AppImage / distroboxneeds a sandbox helper to nestnests 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

FormatBacking crateDecodeEncodeNotes
PNG / APNGpngyesyesanimation
JPEGjpeg-decoderyesyes
GIFgifyesyesanimation
WebPimage-webpyesyesanimation
TIFFtiffyesyes
BMPimageyesyes
ICO / CURimageyes-picks largest entry
TGAimageyes-
QOIqoiyes-
OpenEXRimage (exr)yes-16 / 32-bit float, HDR-aware
PNM familyimageyes-
DDSimageyes-
JPEG XLjxl-oxideyes-
SVGresvg / usvgyes-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:

LayerDefaultWhat it doesFailure surface
landlockondenies all FS paths to the worker; on V4+ also TCP bind/connect; on V6+ scopes abstract-unix-socket and signalsUnsupported on pre-5.13 kernels
seccomponBPF allowlist; everything else returns EPERMUnsupported if prctl fails
rlimitoffRLIMIT_AS and RLIMIT_CPU from LimitsPartiallyApplied 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:

FieldDefault
max_width32768
max_height32768
max_pixels256 Mpx
max_frames1024
max_animation_duration60s
decode_memory_mib512 (RLIMIT_AS if rlimit on)
decode_cpu_seconds30 (RLIMIT_CPU if rlimit on)

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

Feature flags

GroupDefaultNotes
Capabilitydecode, metadataenable encode for PNG, JPEG, GIF, WebP, TIFF, BMP
Sandboxlandlock, seccomp (Linux)toggling off is supported for portability testing, not as a production posture
Per-formatpng, jpeg, gif, webp, tiff, bmp, ico, tga, qoi, exr, pnm, dds, jxl, svgtrim individually
ABI(off) c-apienables 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
  • 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.