Skip to content

Commit 631fc3d

Browse files
author
sahilleth
committed
Guard api/xml and api/json against OutOfMemoryError
Add a heap check before processing potentially large API responses. When free memory is below a configurable threshold (default 50MB), the request is rejected with HTTP 503 and a message suggesting use of the tree parameter to limit returned data. - System property hudson.model.Api.minFreeMemoryBytes to configure or disable (set to 0) - Applies to XML, JSON, and Python API endpoints - Add ApiTest.lowHeapRejectsApiRequest to verify rejection behavior JENKINS-75747 Made-with: Cursor
1 parent 057aa6a commit 631fc3d

File tree

3 files changed

+77
-0
lines changed

3 files changed

+77
-0
lines changed

core/src/main/java/hudson/model/Api.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import jenkins.model.Jenkins;
4343
import jenkins.security.SecureRequester;
4444
import jenkins.security.stapler.StaplerNotDispatchable;
45+
import jenkins.util.SystemProperties;
4546
import jenkins.util.xml.FilteredFunctionContext;
4647
import org.dom4j.CharacterData;
4748
import org.dom4j.Document;
@@ -108,6 +109,10 @@ public void doXml(StaplerRequest2 req, StaplerResponse2 rsp,
108109
@QueryParameter int depth) throws IOException, ServletException {
109110
setHeaders(rsp);
110111

112+
if (!checkHeapForApiResponse(rsp)) {
113+
return;
114+
}
115+
111116
String[] excludes = req.getParameterValues("exclude");
112117

113118
if (xpath == null && excludes == null) {
@@ -256,6 +261,9 @@ public void doJson(StaplerRequest req, StaplerResponse rsp) throws IOException,
256261
private void doJsonImpl(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException, ServletException {
257262
if (req.getParameter("jsonp") == null || permit(req)) {
258263
setHeaders(rsp);
264+
if (!checkHeapForApiResponse(rsp)) {
265+
return;
266+
}
259267
rsp.serveExposedBean(req, bean, req.getParameter("jsonp") == null ? Flavor.JSON : Flavor.JSONP);
260268
} else {
261269
rsp.sendError(HttpURLConnection.HTTP_FORBIDDEN, "jsonp forbidden; implement jenkins.security.SecureRequester");
@@ -292,6 +300,9 @@ public void doPython(StaplerRequest req, StaplerResponse rsp) throws IOException
292300

293301
private void doPythonImpl(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException, ServletException {
294302
setHeaders(rsp);
303+
if (!checkHeapForApiResponse(rsp)) {
304+
return;
305+
}
295306
rsp.serveExposedBean(req, bean, Flavor.PYTHON);
296307
}
297308

@@ -304,6 +315,41 @@ private boolean permit(StaplerRequest2 req) {
304315
return false;
305316
}
306317

318+
/**
319+
* Checks if there is sufficient heap memory before processing a potentially large API response.
320+
* Large serialization operations (especially without the tree parameter) can cause OutOfMemoryError.
321+
*
322+
* @param rsp the response to send error to if heap is low
323+
* @return true if it is safe to proceed, false if the request was rejected
324+
* @see <a href="https://issues.jenkins.io/browse/JENKINS-75747">JENKINS-75747</a>
325+
*/
326+
private boolean checkHeapForApiResponse(StaplerResponse2 rsp) throws IOException {
327+
long minFreeBytes = SystemProperties.getLong(Api.class.getName() + ".minFreeMemoryBytes", 50L * 1024 * 1024);
328+
if (minFreeBytes <= 0) {
329+
return true;
330+
}
331+
332+
Runtime rt = Runtime.getRuntime();
333+
long maxMemory = rt.maxMemory();
334+
long totalMemory = rt.totalMemory();
335+
long freeMemory = rt.freeMemory();
336+
long usedMemory = totalMemory - freeMemory;
337+
long availableMemory = maxMemory - usedMemory;
338+
339+
if (availableMemory >= minFreeBytes) {
340+
return true;
341+
}
342+
343+
LOGGER.warning(() -> String.format(
344+
"Rejecting API request due to low heap: available=%d, required=%d. Suggest using the tree parameter.",
345+
availableMemory, minFreeBytes));
346+
347+
rsp.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
348+
rsp.setContentType("text/plain;charset=UTF-8");
349+
rsp.getWriter().print(Messages.Api_LowMemory());
350+
return false;
351+
}
352+
307353
@Restricted(NoExternalUse.class)
308354
protected void setHeaders(StaplerResponse2 rsp) {
309355
rsp.setHeader("X-Jenkins", Jenkins.VERSION);

core/src/main/resources/hudson/model/Messages.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ Api.MultipleMatch=XPath "{0}" matched {1} nodes. \
8181
Create XPath that only matches one, or use the "wrapper" query parameter to wrap them all under a root element.
8282
Api.NoXPathMatch=XPath {0} didn’t match
8383
Api.WrapperParamInvalid=The wrapper parameter can only contain alphanumeric characters or dash/dot/underscore. The first character has to be a letter or underscore.
84+
Api.LowMemory=Insufficient heap memory to process this API request. The server may run out of memory when serializing large responses. To avoid this, use the tree parameter to limit the data returned, e.g. tree=jobs[name],builds[number]. See the API documentation for details.
8485

8586
BallColor.Aborted=Aborted
8687
BallColor.Disabled=Disabled

test/src/test/java/hudson/model/ApiTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,36 @@ void escapedParameter() throws Exception {
189189
page.getWebResponse().getContentAsString());
190190
}
191191

192+
@Test
193+
@Issue("JENKINS-75747")
194+
void lowHeapRejectsApiRequest() throws Exception {
195+
// Force the OOM guard to reject by requiring more free memory than available
196+
String prop = Api.class.getName() + ".minFreeMemoryBytes";
197+
String original = System.getProperty(prop);
198+
try {
199+
System.setProperty(prop, String.valueOf(Long.MAX_VALUE));
200+
201+
JenkinsRule.WebClient wc = j.createWebClient();
202+
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
203+
204+
Page xmlPage = wc.goTo("api/xml", null);
205+
WebResponse xmlResponse = xmlPage.getWebResponse();
206+
assertEquals(HttpServletResponse.SC_SERVICE_UNAVAILABLE, xmlResponse.getStatusCode());
207+
assertThat(xmlResponse.getContentAsString(), containsString("tree parameter"));
208+
209+
Page jsonPage = wc.goTo("api/json", null);
210+
WebResponse jsonResponse = jsonPage.getWebResponse();
211+
assertEquals(HttpServletResponse.SC_SERVICE_UNAVAILABLE, jsonResponse.getStatusCode());
212+
assertThat(jsonResponse.getContentAsString(), containsString("tree parameter"));
213+
} finally {
214+
if (original != null) {
215+
System.setProperty(prop, original);
216+
} else {
217+
System.clearProperty(prop);
218+
}
219+
}
220+
}
221+
192222
@Test
193223
@Issue("SECURITY-1704")
194224
void project_notExposedToIFrame() throws Exception {

0 commit comments

Comments
 (0)