Skip to content

Commit 4a0200c

Browse files
committed
8367561: Getting some "header" property from a file:// URL causes a file descriptor leak
Reviewed-by: dfuchs, vyazici
1 parent 3cbcda5 commit 4a0200c

File tree

4 files changed

+443
-37
lines changed

4 files changed

+443
-37
lines changed

src/java.base/share/classes/sun/net/www/URLConnection.java

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 1995, 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 1995, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -101,23 +101,35 @@ public Map<String,List<String>> getRequestProperties() {
101101
return Collections.emptyMap();
102102
}
103103

104+
/**
105+
* This method is called whenever the headers related methods are called on the
106+
* {@code URLConnection}. This method does any necessary checks and initializations
107+
* to make sure that the headers can be served. If this {@code URLConnection} cannot
108+
* serve the headers, then this method throws an {@code IOException}.
109+
*
110+
* @throws IOException if the headers cannot be served
111+
*/
112+
protected void ensureCanServeHeaders() throws IOException {
113+
getInputStream();
114+
}
115+
104116
public String getHeaderField(String name) {
105117
try {
106-
getInputStream();
118+
ensureCanServeHeaders();
107119
} catch (Exception e) {
108120
return null;
109121
}
110122
return properties == null ? null : properties.findValue(name);
111123
}
112124

113125

114-
Map<String, List<String>> headerFields;
126+
private Map<String, List<String>> headerFields;
115127

116128
@Override
117129
public Map<String, List<String>> getHeaderFields() {
118130
if (headerFields == null) {
119131
try {
120-
getInputStream();
132+
ensureCanServeHeaders();
121133
if (properties == null) {
122134
headerFields = super.getHeaderFields();
123135
} else {
@@ -137,7 +149,7 @@ public Map<String, List<String>> getHeaderFields() {
137149
*/
138150
public String getHeaderFieldKey(int n) {
139151
try {
140-
getInputStream();
152+
ensureCanServeHeaders();
141153
} catch (Exception e) {
142154
return null;
143155
}
@@ -152,7 +164,7 @@ public String getHeaderFieldKey(int n) {
152164
*/
153165
public String getHeaderField(int n) {
154166
try {
155-
getInputStream();
167+
ensureCanServeHeaders();
156168
} catch (Exception e) {
157169
return null;
158170
}
@@ -221,7 +233,7 @@ public void setContentType(String type) {
221233
*/
222234
public int getContentLength() {
223235
try {
224-
getInputStream();
236+
ensureCanServeHeaders();
225237
} catch (Exception e) {
226238
return -1;
227239
}

src/java.base/share/classes/sun/net/www/protocol/file/FileURLConnection.java

Lines changed: 93 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,30 @@
2525

2626
package sun.net.www.protocol.file;
2727

28+
import java.io.BufferedInputStream;
29+
import java.io.ByteArrayInputStream;
30+
import java.io.File;
31+
import java.io.FileInputStream;
32+
import java.io.FileNotFoundException;
33+
import java.io.FilePermission;
34+
import java.io.IOException;
35+
import java.io.InputStream;
36+
import java.net.FileNameMap;
2837
import java.net.MalformedURLException;
2938
import java.net.URL;
30-
import java.net.FileNameMap;
31-
import java.io.*;
32-
import java.text.Collator;
3339
import java.security.Permission;
34-
import sun.net.www.*;
35-
import java.util.*;
40+
import java.text.Collator;
3641
import java.text.SimpleDateFormat;
42+
import java.util.Arrays;
43+
import java.util.Date;
44+
import java.util.List;
45+
import java.util.Locale;
46+
import java.util.Map;
47+
import java.util.TimeZone;
48+
49+
import sun.net.www.MessageHeader;
50+
import sun.net.www.ParseUtil;
51+
import sun.net.www.URLConnection;
3752

3853
/**
3954
* Open a file input stream given a URL.
@@ -67,25 +82,49 @@ protected FileURLConnection(URL u, File file) {
6782
this.file = file;
6883
}
6984

70-
/*
85+
/**
86+
* If already connected, then this method is a no-op.
87+
* If not already connected, then this method does
88+
* readability checks for the File.
89+
* <p>
90+
* If the File is a directory then the readability check
91+
* is done by verifying that File.list() does not return
92+
* null. On the other hand, if the File is not a directory,
93+
* then this method constructs a temporary FileInputStream
94+
* for the File and lets the FileInputStream's constructor
95+
* implementation do the necessary readability checks.
96+
* That temporary FileInputStream is closed before returning
97+
* from this method.
98+
* <p>
99+
* In either case, if the readability checks fail, then
100+
* an IOException is thrown from this method and the
101+
* FileURLConnection stays unconnected.
102+
* <p>
103+
* A normal return from this method implies that the
104+
* FileURLConnection is connected and the readability
105+
* checks have passed for the File.
106+
* <p>
71107
* Note: the semantics of FileURLConnection object is that the
72108
* results of the various URLConnection calls, such as
73109
* getContentType, getInputStream or getContentLength reflect
74110
* whatever was true when connect was called.
75111
*/
112+
@Override
76113
public void connect() throws IOException {
77114
if (!connected) {
78-
79115
isDirectory = file.isDirectory();
116+
// verify readability of the directory or the regular file
80117
if (isDirectory) {
81118
String[] fileList = file.list();
82-
if (fileList == null)
119+
if (fileList == null) {
83120
throw new FileNotFoundException(file.getPath() + " exists, but is not accessible");
121+
}
84122
directoryListing = Arrays.asList(fileList);
85123
} else {
86-
is = new BufferedInputStream(new FileInputStream(file.getPath()));
124+
// let FileInputStream constructor do the necessary readability checks
125+
// and propagate any failures
126+
new FileInputStream(file.getPath()).close();
87127
}
88-
89128
connected = true;
90129
}
91130
}
@@ -112,9 +151,9 @@ private void initializeHeaders() {
112151
FileNameMap map = java.net.URLConnection.getFileNameMap();
113152
String contentType = map.getContentTypeFor(file.getPath());
114153
if (contentType != null) {
115-
properties.add(CONTENT_TYPE, contentType);
154+
properties.set(CONTENT_TYPE, contentType);
116155
}
117-
properties.add(CONTENT_LENGTH, Long.toString(length));
156+
properties.set(CONTENT_LENGTH, Long.toString(length));
118157

119158
/*
120159
* Format the last-modified field into the preferred
@@ -126,85 +165,109 @@ private void initializeHeaders() {
126165
SimpleDateFormat fo =
127166
new SimpleDateFormat ("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US);
128167
fo.setTimeZone(TimeZone.getTimeZone("GMT"));
129-
properties.add(LAST_MODIFIED, fo.format(date));
168+
properties.set(LAST_MODIFIED, fo.format(date));
130169
}
131170
} else {
132-
properties.add(CONTENT_TYPE, TEXT_PLAIN);
171+
properties.set(CONTENT_TYPE, TEXT_PLAIN);
133172
}
134173
initializedHeaders = true;
135174
}
136175
}
137176

138-
public Map<String,List<String>> getHeaderFields() {
177+
@Override
178+
public Map<String, List<String>> getHeaderFields() {
139179
initializeHeaders();
140180
return super.getHeaderFields();
141181
}
142182

183+
@Override
143184
public String getHeaderField(String name) {
144185
initializeHeaders();
145186
return super.getHeaderField(name);
146187
}
147188

189+
@Override
148190
public String getHeaderField(int n) {
149191
initializeHeaders();
150192
return super.getHeaderField(n);
151193
}
152194

195+
@Override
153196
public int getContentLength() {
154197
initializeHeaders();
155198
if (length > Integer.MAX_VALUE)
156199
return -1;
157200
return (int) length;
158201
}
159202

203+
@Override
160204
public long getContentLengthLong() {
161205
initializeHeaders();
162206
return length;
163207
}
164208

209+
@Override
165210
public String getHeaderFieldKey(int n) {
166211
initializeHeaders();
167212
return super.getHeaderFieldKey(n);
168213
}
169214

215+
@Override
170216
public MessageHeader getProperties() {
171217
initializeHeaders();
172218
return super.getProperties();
173219
}
174220

221+
@Override
175222
public long getLastModified() {
176223
initializeHeaders();
177224
return lastModified;
178225
}
179226

227+
@Override
180228
public synchronized InputStream getInputStream()
181229
throws IOException {
182230

183231
connect();
232+
// connect() does the necessary readability checks and is expected to
233+
// throw IOException if any of those checks fail. A normal completion of connect()
234+
// must mean that connect succeeded.
235+
assert connected : "not connected";
184236

185-
if (is == null) {
186-
if (isDirectory) {
237+
// a FileURLConnection only ever creates and provides a single InputStream
238+
if (is != null) {
239+
return is;
240+
}
187241

188-
if (directoryListing == null) {
189-
throw new FileNotFoundException(file.getPath());
190-
}
242+
if (isDirectory) {
243+
// a successful connect() implies the directoryListing is non-null
244+
// if the file is a directory
245+
assert directoryListing != null : "missing directory listing";
191246

192-
directoryListing.sort(Collator.getInstance());
247+
directoryListing.sort(Collator.getInstance());
193248

194-
StringBuilder sb = new StringBuilder();
195-
for (String fileName : directoryListing) {
196-
sb.append(fileName);
197-
sb.append("\n");
198-
}
199-
// Put it into a (default) locale-specific byte-stream.
200-
is = new ByteArrayInputStream(sb.toString().getBytes());
201-
} else {
202-
throw new FileNotFoundException(file.getPath());
249+
StringBuilder sb = new StringBuilder();
250+
for (String fileName : directoryListing) {
251+
sb.append(fileName);
252+
sb.append("\n");
203253
}
254+
// Put it into a (default) locale-specific byte-stream.
255+
is = new ByteArrayInputStream(sb.toString().getBytes());
256+
} else {
257+
is = new BufferedInputStream(new FileInputStream(file.getPath()));
204258
}
205259
return is;
206260
}
207261

262+
@Override
263+
protected synchronized void ensureCanServeHeaders() throws IOException {
264+
// connect() (if not already connected) does the readability checks
265+
// and throws an IOException if those checks fail. A successful
266+
// completion from connect() implies the File is readable.
267+
connect();
268+
}
269+
270+
208271
Permission permission;
209272

210273
/* since getOutputStream isn't supported, only read permission is

0 commit comments

Comments
 (0)