Skip to content

Commit 0484fdc

Browse files
Merge pull request #411 from bernardladenthin/claude/java8-safe-logging
fix!: ship a Java 8 loadable SLF4J binding and checker-qual
2 parents 519ff86 + 18f225c commit 0484fdc

5 files changed

Lines changed: 209 additions & 9 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,43 @@ from version 5.0.0 onward. Pre-fork releases (`1.x`–`4.2.0`) were authored by
99

1010
## [Unreleased]
1111

12+
### Changed
13+
- **BREAKING (runtime): the shipped SLF4J binding is now `slf4j-simple`, not `logback-classic`.**
14+
Two independent reasons, and the first is a hard failure rather than a preference:
15+
16+
1. **This artifact targets Java 8 and logback no longer does.** Every logback release from 1.4.0 on
17+
is class-file major 55 (Java 11). SLF4J's `ServiceLoader` loads `LogbackServiceProvider` at JVM
18+
startup, so a Java 8 consumer got `UnsupportedClassVersionError` before a single line of library
19+
code ran. Measured: all 181 classes of logback-classic 1.6.3 are major 55.
20+
2. **The Java 8 logback line is end-of-life with unfixed CVEs.** 1.3.16 (2025-10-29) is its last
21+
release; CVE-2026-1225, CVE-2026-9828 and CVE-2026-10532 were fixed only in 1.5.x, and
22+
CVE-2026-19880 only in 1.6.3 — which is Java 11 bytecode and therefore unreachable from here.
23+
Downgrading would have traded a crash for permanent unpatchability.
24+
25+
`slf4j-simple` is six classes from the same release train as `slf4j-api`, with no configuration
26+
parser, socket server or deserialization — the subsystems essentially every logback CVE lives in.
27+
28+
**What changes for you:** `logback.xml` is no longer read. Configure with a classpath
29+
`simplelogger.properties` or `-Dorg.slf4j.simpleLogger.*`. With no configuration at all, output is
30+
quieter than before (logback defaulted the root logger to DEBUG; slf4j-simple defaults to INFO).
31+
To keep logback, exclude `org.slf4j:slf4j-simple` and declare your own binding — which is what the
32+
SLF4J api/binding split is for.
33+
34+
**The runnable fat jar carries logging defaults; the library jar deliberately does not.**
35+
`simplelogger.properties` (INFO, stdout, timestamps, thread + short logger name — what the previous
36+
logback default emitted) is added by the assembly, from `src/main/assembly-resources/`. It is *not*
37+
under `src/main/resources`, because from there it would be published inside the library jar and land
38+
on every consumer's classpath: slf4j-simple reads whichever file the classloader hands it first, so
39+
a consumer with their own configuration would get a coin flip. A library must not decide that. The
40+
`assembly` profile therefore uses its own descriptor — a verbatim copy of the predefined
41+
`jar-with-dependencies` plus that one file.
42+
43+
- **`checker-qual` pinned to 3.55.1 and marked optional.** 4.x is major 55 and its annotations are
44+
`@Retention(RUNTIME)`, so a Java 8 JVM throws `UnsupportedClassVersionError` the moment anything
45+
reflects over an annotated element. The optional flag keeps it out of consumers' transitive graph;
46+
the version pin is what protects the fat jar, since `jar-with-dependencies` filters on scope only.
47+
The build-time Checker Framework processor stays on 4.2.2 under its own property.
48+
1249
### Added
1350
- **`ModelParameters.setFlashAttn(FlashAttn)` — the only way to express `--flash-attn` correctly.**
1451
llama.cpp turned that option from a bare flag into a value-taking one in **b10273**: the

‎CLAUDE.md‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1620,6 +1620,32 @@ EXPECT_FALSE(j.contains("stop_type")); // filtered out
16201620
16211621
See [`../workspace/policies/javadoc-conventions.md`](../workspace/policies/javadoc-conventions.md).
16221622
1623+
## Java 8 bytecode floor — what may ship
1624+
1625+
This artifact targets **Java 8** (`release 8`), so **every class a consumer's JVM can load must be
1626+
class-file major 52 or lower**. Two entries in `llama/pom.xml` exist only for that, and both are
1627+
easy to undo by accident:
1628+
1629+
- **`slf4j-simple`, not logback, is the shipped SLF4J binding.** Every logback release from 1.4.0 on
1630+
is Java 11 bytecode, so `LogbackServiceProvider` cannot load on Java 8 — SLF4J's `ServiceLoader`
1631+
finds it at startup and the JVM throws `UnsupportedClassVersionError`. The Java 8 line (1.3.x) is
1632+
end-of-life (last release 1.3.16, 2025-10-29) and every logback CVE disclosed since has been fixed
1633+
only in 1.5.x/1.6.x with no backport, so it is not an option either. `slf4j-simple` is six classes
1634+
from the same release train as `slf4j-api`, with no configuration or socket layer for a CVE to
1635+
live in. Configure it with a classpath `simplelogger.properties` or `-Dorg.slf4j.simpleLogger.*`.
1636+
- **`checker.qual.version` (3.55.1) is a separate property from `checker.version` (the build-time
1637+
processor).** checker-qual 4.x is major 55, its annotations are `@Retention(RUNTIME)`, and anything
1638+
reflecting over an annotated element (Jackson does) loads them. `<optional>true</optional>` keeps
1639+
it out of consumers' transitive graph but **not** out of the fat jar — `jar-with-dependencies`
1640+
filters on scope only — so the version pin is what protects the shipped artifact. Never collapse
1641+
the two properties back into one: the processor runs on the CI JDK and must stay current.
1642+
1643+
**Surefire excludes `org.slf4j:slf4j-simple` from the test classpath** (`classpathDependencyExcludes`).
1644+
Runtime scope is on the test classpath too, and LogCaptor (test scope) requires logback specifically —
1645+
with both providers present it fails with *"SLF4J Logger implementation should be of the type
1646+
[ch.qos.logback.classic.Logger]"*. The exclusion leaves logback the sole provider in tests and does
1647+
not touch the artifact.
1648+
16231649
## SpotBugs Suppressions
16241650
16251651
See [`../workspace/policies/spotbugs-suppressions.md`](../workspace/policies/spotbugs-suppressions.md).
@@ -1828,7 +1854,7 @@ the recommended path (README "Importing in Android", Option 1):
18281854
change was needed. Built by the **standalone plain-Gradle build** in `llama-android/`
18291855
(see "Repository layout" for why it is not a Maven module); the POM mirrors the core's
18301856
compile-scope deps (jackson/slf4j-api/jspecify/checker-qual, versions parsed from
1831-
`llama/pom.xml` — deliberately NOT logback, which is the JVM-only runtime binding).
1857+
`llama/pom.xml` — deliberately NOT the SLF4J binding, which is the JVM-only runtime dependency).
18321858
- **`net.ladenthin:llama-kotlin`** — Maven reactor module; pure-Kotlin (2.4, jvmTarget 1.8)
18331859
coroutines façade: `generateFlow`/`generateChatFlow` (cold `Flow`, source closed on
18341860
completion/error/cancellation) and `completeSuspend`/`chatSuspend`/`chatCompleteTextSuspend`/

‎llama/pom.xml‎

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,14 @@ SPDX-License-Identifier: MIT
5959
<lombok.version>1.18.46</lombok.version>
6060
<errorprone.version>2.50.0</errorprone.version>
6161
<nullaway.version>0.14.0</nullaway.version>
62+
<!-- Build-time Checker Framework processor. Runs on the CI JDK, never ships, so it
63+
tracks the newest release. Deliberately NOT the same property as the annotations
64+
below: those are shipped and must stay Java 8 bytecode. -->
6265
<checker.version>4.2.2</checker.version>
66+
<!-- Shipped checker-qual annotations. Last release whose classes are class-file
67+
major 52; 4.0.0 moved the line to Java 11. See the dependency for why the pin,
68+
not just the optional flag, is what protects the artifact. -->
69+
<checker.qual.version>3.55.1</checker.qual.version>
6370
<jackson.version>2.22.2</jackson.version>
6471
<reactor.version>3.8.7</reactor.version>
6572
<slf4j.version>2.0.18</slf4j.version>
@@ -183,10 +190,20 @@ SPDX-License-Identifier: MIT
183190
<artifactId>jspecify</artifactId>
184191
<version>${jspecify.version}</version>
185192
</dependency>
193+
<!-- Pinned to the newest Java 8 line on purpose. checker-qual 4.x is Java 11
194+
bytecode (class-file major 55) and this artifact targets Java 8; its
195+
annotations are @Retention(RUNTIME), so anything reflecting over an
196+
annotated element (Jackson does) loads them and a Java 8 JVM then throws
197+
UnsatisfiedClassVersionError. Marking it optional keeps it out of
198+
consumers' transitive graph but NOT out of the fat jar; the
199+
jar-with-dependencies descriptor filters on scope only, so the version
200+
pin is the part that actually protects the shipped artifact.
201+
3.55.1 is the last release whose classes are major 52; the break is at 4.0.0. -->
186202
<dependency>
187203
<groupId>org.checkerframework</groupId>
188204
<artifactId>checker-qual</artifactId>
189-
<version>${checker.version}</version>
205+
<version>${checker.qual.version}</version>
206+
<optional>true</optional>
190207
</dependency>
191208
<dependency>
192209
<groupId>com.fasterxml.jackson.core</groupId>
@@ -200,11 +217,34 @@ SPDX-License-Identifier: MIT
200217
<version>${slf4j.version}</version>
201218
</dependency>
202219
<!-- Default SLF4J binding shipped with this library. Runtime scope: not
203-
required on the compile classpath, only loaded at JVM startup. -->
220+
required on the compile classpath, only loaded at JVM startup.
221+
222+
slf4j-simple rather than logback, for two independent reasons:
223+
224+
(1) Java 8. Every logback release from 1.4.0 on is Java 11 bytecode, so
225+
LogbackServiceProvider cannot load on the Java 8 this artifact targets:
226+
SLF4J's ServiceLoader finds it at startup and the JVM throws
227+
UnsupportedClassVersionError. The Java 8 line (1.3.x) would fix that but
228+
is end-of-life: 1.3.16 (2025-10-29) is its last release, and every logback
229+
CVE disclosed since has been fixed only in 1.5.x/1.6.x with no backport
230+
(CVE-2026-1225, CVE-2026-9828, CVE-2026-10532; CVE-2026-19880 is fixed only in 1.6.3,
231+
which is Java 11 bytecode and therefore unreachable from here).
232+
233+
(2) Attack surface. Essentially every logback CVE lives in its configuration
234+
or socket layers: Janino expression evaluation, HardenedObjectInputStream,
235+
SaxEventRecorder, SocketReceiver. slf4j-simple is six classes with no
236+
config parser, no socket server and no deserialization, so those classes
237+
of defect cannot exist in it. It also ships in the same release train as
238+
slf4j-api above, so the two can never drift apart.
239+
240+
What consumers lose: no logback.xml. Configure via a classpath
241+
simplelogger.properties or -Dorg.slf4j.simpleLogger.* system properties.
242+
Anyone who wants logback (or any other binding) excludes this one and
243+
declares their own; that is the point of the SLF4J split. -->
204244
<dependency>
205-
<groupId>ch.qos.logback</groupId>
206-
<artifactId>logback-classic</artifactId>
207-
<version>${logback.version}</version>
245+
<groupId>org.slf4j</groupId>
246+
<artifactId>slf4j-simple</artifactId>
247+
<version>${slf4j.version}</version>
208248
<scope>runtime</scope>
209249
</dependency>
210250
<!-- @IgnoreJRERequirement marker used by OSInfo (vendored from xerial/sqlite-jdbc)
@@ -651,6 +691,26 @@ SPDX-License-Identifier: MIT
651691
<configuration>
652692
<!-- -XX:+EnableDynamicAgentLoading: silences the JDK 21 byte-buddy self-attach agent warning that intermittently corrupts Surefire's fork channel ("Corrupted channel ..." / bogus "timeout in the fork"). See workspace policy ci-test-diagnostics.md section 2.1: https://github.com/bernardladenthin/workspace/blob/main/policies/ci-test-diagnostics.md - root cause: https://github.com/raphw/byte-buddy/issues/1639 -->
653693
<argLine>@{argLine} -Xmx2g -XX:ErrorFile=hs_err_pid%p.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=. -XX:+EnableDynamicAgentLoading</argLine>
694+
<!--
695+
Keep the shipped SLF4J binding OFF the test classpath.
696+
697+
slf4j-simple is a runtime-scope dependency, and runtime scope is part of
698+
the test classpath too. LogCaptor (io.github.hakky54, test scope) brings
699+
logback, so both providers would be present, SLF4J would pick one
700+
arbitrarily, and LogCaptor fails outright when it does not get logback:
701+
"SLF4J Logger implementation should be of the type
702+
[ch.qos.logback.classic.Logger] but found [org.slf4j.simple.SimpleLogger]"
703+
(7 tests across LoggingSmokeTest, TimingsLoggerTest, ChatResponseParserTest
704+
and CompletionResponseParserTest).
705+
706+
Excluding it here leaves logback as the sole provider during tests and does
707+
not touch the artifact: the shipped jar and the fat jar still carry
708+
slf4j-simple. The tests assert what our code logs, which is binding-agnostic;
709+
which binding renders it is not what they are about.
710+
-->
711+
<classpathDependencyExcludes>
712+
<classpathDependencyExclude>org.slf4j:slf4j-simple</classpathDependencyExclude>
713+
</classpathDependencyExcludes>
654714
<!--
655715
Capture each test class's stdout/stderr into
656716
target/surefire-reports/<class>-output.txt. When a native crash
@@ -2145,9 +2205,12 @@ SPDX-License-Identifier: MIT
21452205
<groupId>org.apache.maven.plugins</groupId>
21462206
<artifactId>maven-assembly-plugin</artifactId>
21472207
<configuration>
2148-
<descriptorRefs>
2149-
<descriptorRef>jar-with-dependencies</descriptorRef>
2150-
</descriptorRefs>
2208+
<!-- Our own descriptor, not the predefined jar-with-dependencies ref: it is
2209+
a verbatim copy of that one plus simplelogger.properties, which must
2210+
reach the runnable jar without being published in the library jar. -->
2211+
<descriptors>
2212+
<descriptor>src/assembly/fat-jar.xml</descriptor>
2213+
</descriptors>
21512214
<archive>
21522215
<manifest>
21532216
<mainClass>net.ladenthin.llama.server.ServerLauncher</mainClass>

‎llama/src/assembly/fat-jar.xml‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
4+
5+
SPDX-License-Identifier: MIT
6+
-->
7+
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0"
8+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
9+
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0 http://maven.apache.org/xsd/assembly-2.2.0.xsd">
10+
<!--
11+
Same output as the predefined jar-with-dependencies descriptor, plus one file.
12+
13+
The dependencySet below is a verbatim copy of the predefined descriptor shipped
14+
inside maven-assembly-plugin: outputDirectory /, useProjectArtifact true,
15+
unpack true, scope runtime. Keep it that way; the only reason this file exists at
16+
all is the fileSet underneath it.
17+
18+
That fileSet adds simplelogger.properties, which cannot live in src/main/resources:
19+
from there it would be published in the library jar and land on every consumer's
20+
classpath, where slf4j-simple would read it instead of theirs. Only the runnable
21+
artifact may carry logging defaults.
22+
-->
23+
<id>jar-with-dependencies</id>
24+
<formats>
25+
<format>jar</format>
26+
</formats>
27+
<includeBaseDirectory>false</includeBaseDirectory>
28+
<dependencySets>
29+
<dependencySet>
30+
<outputDirectory>/</outputDirectory>
31+
<useProjectArtifact>true</useProjectArtifact>
32+
<unpack>true</unpack>
33+
<scope>runtime</scope>
34+
</dependencySet>
35+
</dependencySets>
36+
<fileSets>
37+
<fileSet>
38+
<directory>${project.basedir}/src/main/assembly-resources</directory>
39+
<outputDirectory>/</outputDirectory>
40+
<includes>
41+
<include>simplelogger.properties</include>
42+
</includes>
43+
</fileSet>
44+
</fileSets>
45+
</assembly>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
# slf4j-simple defaults for the RUNNABLE FAT JAR only.
6+
#
7+
# This file is deliberately NOT under src/main/resources: that would put it into the
8+
# published library jar, where every consumer of net.ladenthin:llama would find our
9+
# logging configuration on their classpath. slf4j-simple reads whichever
10+
# simplelogger.properties the classloader hands it first, so a consumer with their own
11+
# file would get a coin flip. A library must not decide that; an application may, and
12+
# the fat jar is the application.
13+
#
14+
# Every setting here is also overridable at launch with -Dorg.slf4j.simpleLogger.<key>.
15+
16+
# INFO keeps startup and per-request lines without the native layer's debug chatter.
17+
org.slf4j.simpleLogger.defaultLogLevel=info
18+
19+
# stdout, not the slf4j-simple default of stderr: the server's own output belongs on the
20+
# same stream as everything else a user pipes or redirects.
21+
org.slf4j.simpleLogger.logFile=System.out
22+
23+
# Wall-clock timestamps -- a server log without them is hard to correlate with anything.
24+
org.slf4j.simpleLogger.showDateTime=true
25+
org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss.SSS
26+
27+
# Thread and short logger name, mirroring what the previous logback default emitted.
28+
org.slf4j.simpleLogger.showThreadName=true
29+
org.slf4j.simpleLogger.showShortLogName=true

0 commit comments

Comments
 (0)