From 607eaae3a92e3703fd016bebd2dbcc4e3afc17ed Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Fri, 22 Nov 2024 12:59:46 -0600 Subject: [PATCH] even more defensive programming on image handling Signed-off-by: eternal-flame-AD --- README.md | 16 ++++++-- src/lib.rs | 23 ++++++++++++ src/main.rs | 2 + src/post_process/image_processing.rs | 38 +++++++++++++++++-- src/sandbox.rs | 56 +++++++++++++++++++++++++--- 5 files changed, 122 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index cd7bc7a..4695064 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,12 @@ Work in progress! Currently to do: - [X] RFC9110 compliant proxy loop detection with defensive programming against known vulnerable proxies - [X] HTTPs only mode and X-Forwarded-Proto reflection - [X] Cache-Control header -- [X] Rate-limiting on local deployment (untested) +- [X] Rate-limiting on local deployment - [X] Read config from Cloudflare - [X] Timing and Rate-limiting headers (some not available on Cloudflare Workers) - [X] Tiered rate-limiting - [ ] Lossy WebP on CF Workers (maybe already works?) -- [ ] Cache Results on Cloudflare KV. +- [ ] Cache results - [ ] Handle all possible panics reported by Clippy - [X] Sandboxing the image rendering - [X] Prometheus-format metrics @@ -29,21 +29,29 @@ Work in progress! Currently to do: Preview at: -[https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png) +CF Worker: [https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png) + +Local: [https://mproxy.mi.yumechi.jp/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png](https://mproxy.mi.yumechi.jp/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png) Image: ![Syuilo Avatar resived.png](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png) +![Syuilo Avatar resived.png](https://mproxy.mi.yumechi.jp/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png) + ### SVG rendering (font rendering disabled due to size restrictions) -[https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg) +CF Worker: [https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg) + +Local: [https://mproxy.mi.yumechi.jp/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg](https://mproxy.mi.yumechi.jp/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg) ![AES-AddRoundKey.svg](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg) +![AES-AddRoundKey.svg](https://mproxy.mi.yumechi.jp/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg) + ## Setup and Deployment 1. Clone this repository. Load the submodules with `git submodule update --init`. diff --git a/src/lib.rs b/src/lib.rs index 0123405..e3303ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -813,6 +813,29 @@ pub struct App { _marker: PhantomData<(C, S)>, } +#[cfg(feature = "env-local")] +/// Register cancel handler for threads that has ran away for some reason +/// +/// This should only happen when some kind of malformed input is happening that causes the image parser to hang +/// without triggering the resource limits. +#[allow(unsafe_code)] +pub fn register_cancel_handler() { + #[cfg(target_family = "unix")] + unsafe { + unsafe fn cancel_handler(_: libc::c_int) { + log::error!("Received cancel signal, stopping thread"); + libc::pthread_exit(&sandbox::EXIT_TIMEOUT as *const _ as *mut _); + } + + if libc::signal(libc::SIGUSR1, cancel_handler as usize) == libc::SIG_ERR { + log::error!( + "Failed to register cancel handler: {}", + std::io::Error::last_os_error() + ); + } + } +} + #[cfg(any(feature = "cf-worker", feature = "reqwest"))] #[allow(clippy::unused_async)] impl App { diff --git a/src/main.rs b/src/main.rs index 4ba418a..938aa4d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -225,6 +225,8 @@ fn main() { log::info!("Spawned Tokio reactor."); + yumechi_no_kuni_proxy_worker::register_cancel_handler(); + runtime .block_on(async move { #[cfg(not(feature = "axum-server"))] diff --git a/src/post_process/image_processing.rs b/src/post_process/image_processing.rs index 620bc61..9c7e764 100644 --- a/src/post_process/image_processing.rs +++ b/src/post_process/image_processing.rs @@ -2,7 +2,8 @@ use std::io::Cursor; use image::{ codecs::{png::PngDecoder, webp::WebPDecoder}, - AnimationDecoder, DynamicImage, GenericImageView, ImageResult, + AnimationDecoder, DynamicImage, GenericImageView, ImageDecoder, ImageReader, ImageResult, + Limits, }; use crate::ImageOptions; @@ -34,7 +35,9 @@ pub fn postprocess_webp_image( data: &[u8], opt: &ImageOptions, ) -> ImageResult> { - let dec = WebPDecoder::new(Cursor::new(data))?; + let mut dec = WebPDecoder::new(Cursor::new(data))?; + dec.set_limits(default_limits()) + .expect("Failed to set limits"); if !dec.has_animation() && opt.requested_resize() { return Ok(Some(postprocess_static_image(data, opt)?)); @@ -56,7 +59,9 @@ pub fn postprocess_webp_image( /// Postprocesses an PNG image using the given options pub fn postprocess_png_image(data: &[u8], opt: &ImageOptions) -> ImageResult> { - let dec = PngDecoder::new(Cursor::new(data))?; + let mut dec = PngDecoder::new(Cursor::new(data))?; + dec.set_limits(default_limits()) + .expect("Failed to set limits"); let apng = dec.is_apng()?; @@ -81,6 +86,9 @@ pub fn postprocess_png_image(data: &[u8], opt: &ImageOptions) -> ImageResult 25535 { + return Err(SvgPostprocessError::LimitExceeded(complexity)); + } + let size = svg.size(); let clamped = clamp_dimensions((size.width() as u32, size.height() as u32), 800, 800); @@ -144,8 +163,21 @@ pub fn postprocess_svg_image( )) } +fn default_limits() -> Limits { + let mut l = Limits::default(); + l.max_alloc = Some(64 << 20); + l.max_image_height = Some(8192); + l.max_image_width = Some(8192); + + l +} + /// Preprocesses a static image using the given options pub fn postprocess_static_image(data: &[u8], opt: &ImageOptions) -> ImageResult { + let mut r = ImageReader::new(Cursor::new(data)); + + r.limits(default_limits()); + Ok(process_static_image_impl( image::load_from_memory(data)?, opt, diff --git a/src/sandbox.rs b/src/sandbox.rs index 25e14e2..2ca2ec7 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -1,7 +1,49 @@ use crate::SandboxConfig; -#[cfg(all(target_family = "unix", feature = "tokio"))] +/// Thread termination due to timeout +pub const EXIT_TIMEOUT: usize = 1; + +#[cfg(target_os = "linux")] +#[allow(missing_docs)] +pub(crate) mod pthread { + + #[link(name = "pthread")] + extern "C" { + pub fn pthread_setcancelstate(state: i32, oldstate: *mut i32) -> i32; + pub fn pthread_setcanceltype(type_: i32, oldtype: *mut i32) -> i32; + } + + pub const PTHREAD_CANCEL_ENABLE: i32 = 0; + pub const PTHREAD_CANCEL_ASYNCHRONOUS: i32 = 1; +} + +/// Run a function with explicit immediate cancellation #[allow(unsafe_code)] +pub(crate) fn pthread_cancelable R, R>(f: F) -> R { + unsafe { + let mut oldstate = 0; + let mut oldtype = 0; + #[cfg(target_os = "linux")] + pthread::pthread_setcancelstate(pthread::PTHREAD_CANCEL_ENABLE, &mut oldstate); + #[cfg(target_os = "linux")] + pthread::pthread_setcanceltype(pthread::PTHREAD_CANCEL_ASYNCHRONOUS, &mut oldtype); + log::trace!( + "Running cancelable function, oldstate: {}, oldtype: {}", + oldstate, + oldtype + ); + let res = f(); + #[cfg(target_os = "linux")] + pthread::pthread_setcancelstate(oldstate, std::ptr::null_mut()); + #[cfg(target_os = "linux")] + pthread::pthread_setcanceltype(oldtype, std::ptr::null_mut()); + res + } +} + +#[cfg(all(target_family = "unix", feature = "tokio"))] +#[allow(unsafe_code, clippy::inline_always)] +#[inline(always)] /// A bloking tokio task with a cancellation pub fn tokio_block R + Send, R: Send>( sandbox: &impl Sandboxing, @@ -14,17 +56,19 @@ pub fn tokio_block R + Send, R: Send>( tokio::spawn(async move { if signal.await.is_ok() { unsafe { - libc::pthread_cancel(tid); + libc::pthread_kill(tid, libc::SIGUSR1); } } }); tokio::task::block_in_place(move || { - let guard = sandbox.setup(key); - let res = f(); - drop(guard); + pthread_cancelable(|| { + let guard = sandbox.setup(key); + let res = f(); + drop(guard); - res + res + }) }) }