001 /**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements. See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership. The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License. You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018 package org.apache.hadoop.io;
019
020 import java.io.File;
021 import java.io.FileDescriptor;
022 import java.io.FileInputStream;
023 import java.io.FileOutputStream;
024 import java.io.IOException;
025 import java.io.RandomAccessFile;
026 import java.util.Arrays;
027
028 import org.apache.hadoop.conf.Configuration;
029 import org.apache.hadoop.fs.FSDataInputStream;
030 import org.apache.hadoop.fs.FileSystem;
031 import org.apache.hadoop.fs.Path;
032 import org.apache.hadoop.fs.permission.FsPermission;
033 import org.apache.hadoop.io.nativeio.Errno;
034 import org.apache.hadoop.io.nativeio.NativeIO;
035 import org.apache.hadoop.io.nativeio.NativeIOException;
036 import org.apache.hadoop.io.nativeio.NativeIO.POSIX.Stat;
037 import org.apache.hadoop.security.UserGroupInformation;
038
039 import com.google.common.annotations.VisibleForTesting;
040
041 /**
042 * This class provides secure APIs for opening and creating files on the local
043 * disk. The main issue this class tries to handle is that of symlink traversal.
044 * <br/>
045 * An example of such an attack is:
046 * <ol>
047 * <li> Malicious user removes his task's syslog file, and puts a link to the
048 * jobToken file of a target user.</li>
049 * <li> Malicious user tries to open the syslog file via the servlet on the
050 * tasktracker.</li>
051 * <li> The tasktracker is unaware of the symlink, and simply streams the contents
052 * of the jobToken file. The malicious user can now access potentially sensitive
053 * map outputs, etc. of the target user's job.</li>
054 * </ol>
055 * A similar attack is possible involving task log truncation, but in that case
056 * due to an insecure write to a file.
057 * <br/>
058 */
059 public class SecureIOUtils {
060
061 /**
062 * Ensure that we are set up to run with the appropriate native support code.
063 * If security is disabled, and the support code is unavailable, this class
064 * still tries its best to be secure, but is vulnerable to some race condition
065 * attacks.
066 *
067 * If security is enabled but the support code is unavailable, throws a
068 * RuntimeException since we don't want to run insecurely.
069 */
070 static {
071 boolean shouldBeSecure = UserGroupInformation.isSecurityEnabled();
072 boolean canBeSecure = NativeIO.isAvailable();
073
074 if (!canBeSecure && shouldBeSecure) {
075 throw new RuntimeException(
076 "Secure IO is not possible without native code extensions.");
077 }
078
079 // Pre-cache an instance of the raw FileSystem since we sometimes
080 // do secure IO in a shutdown hook, where this call could fail.
081 try {
082 rawFilesystem = FileSystem.getLocal(new Configuration()).getRaw();
083 } catch (IOException ie) {
084 throw new RuntimeException(
085 "Couldn't obtain an instance of RawLocalFileSystem.");
086 }
087
088 // SecureIO just skips security checks in the case that security is
089 // disabled
090 skipSecurity = !canBeSecure;
091 }
092
093 private final static boolean skipSecurity;
094 private final static FileSystem rawFilesystem;
095
096 /**
097 * Open the given File for random read access, verifying the expected user/
098 * group constraints if security is enabled.
099 *
100 * Note that this function provides no additional security checks if hadoop
101 * security is disabled, since doing the checks would be too expensive when
102 * native libraries are not available.
103 *
104 * @param f file that we are trying to open
105 * @param mode mode in which we want to open the random access file
106 * @param expectedOwner the expected user owner for the file
107 * @param expectedGroup the expected group owner for the file
108 * @throws IOException if an IO error occurred or if the user/group does
109 * not match when security is enabled.
110 */
111 public static RandomAccessFile openForRandomRead(File f,
112 String mode, String expectedOwner, String expectedGroup)
113 throws IOException {
114 if (!UserGroupInformation.isSecurityEnabled()) {
115 return new RandomAccessFile(f, mode);
116 }
117 return forceSecureOpenForRandomRead(f, mode, expectedOwner, expectedGroup);
118 }
119
120 /**
121 * Same as openForRandomRead except that it will run even if security is off.
122 * This is used by unit tests.
123 */
124 @VisibleForTesting
125 protected static RandomAccessFile forceSecureOpenForRandomRead(File f,
126 String mode, String expectedOwner, String expectedGroup)
127 throws IOException {
128 RandomAccessFile raf = new RandomAccessFile(f, mode);
129 boolean success = false;
130 try {
131 Stat stat = NativeIO.POSIX.getFstat(raf.getFD());
132 checkStat(f, stat.getOwner(), stat.getGroup(), expectedOwner,
133 expectedGroup);
134 success = true;
135 return raf;
136 } finally {
137 if (!success) {
138 raf.close();
139 }
140 }
141 }
142
143 /**
144 * Opens the {@link FSDataInputStream} on the requested file on local file
145 * system, verifying the expected user/group constraints if security is
146 * enabled.
147 * @param file absolute path of the file
148 * @param expectedOwner the expected user owner for the file
149 * @param expectedGroup the expected group owner for the file
150 * @throws IOException if an IO Error occurred or the user/group does not
151 * match if security is enabled
152 */
153 public static FSDataInputStream openFSDataInputStream(File file,
154 String expectedOwner, String expectedGroup) throws IOException {
155 if (!UserGroupInformation.isSecurityEnabled()) {
156 return rawFilesystem.open(new Path(file.getAbsolutePath()));
157 }
158 return forceSecureOpenFSDataInputStream(file, expectedOwner, expectedGroup);
159 }
160
161 /**
162 * Same as openFSDataInputStream except that it will run even if security is
163 * off. This is used by unit tests.
164 */
165 @VisibleForTesting
166 protected static FSDataInputStream forceSecureOpenFSDataInputStream(
167 File file,
168 String expectedOwner, String expectedGroup) throws IOException {
169 final FSDataInputStream in =
170 rawFilesystem.open(new Path(file.getAbsolutePath()));
171 boolean success = false;
172 try {
173 Stat stat = NativeIO.POSIX.getFstat(in.getFileDescriptor());
174 checkStat(file, stat.getOwner(), stat.getGroup(), expectedOwner,
175 expectedGroup);
176 success = true;
177 return in;
178 } finally {
179 if (!success) {
180 in.close();
181 }
182 }
183 }
184
185 /**
186 * Open the given File for read access, verifying the expected user/group
187 * constraints if security is enabled.
188 *
189 * Note that this function provides no additional checks if Hadoop
190 * security is disabled, since doing the checks would be too expensive
191 * when native libraries are not available.
192 *
193 * @param f the file that we are trying to open
194 * @param expectedOwner the expected user owner for the file
195 * @param expectedGroup the expected group owner for the file
196 * @throws IOException if an IO Error occurred, or security is enabled and
197 * the user/group does not match
198 */
199 public static FileInputStream openForRead(File f, String expectedOwner,
200 String expectedGroup) throws IOException {
201 if (!UserGroupInformation.isSecurityEnabled()) {
202 return new FileInputStream(f);
203 }
204 return forceSecureOpenForRead(f, expectedOwner, expectedGroup);
205 }
206
207 /**
208 * Same as openForRead() except that it will run even if security is off.
209 * This is used by unit tests.
210 */
211 @VisibleForTesting
212 protected static FileInputStream forceSecureOpenForRead(File f, String expectedOwner,
213 String expectedGroup) throws IOException {
214
215 FileInputStream fis = new FileInputStream(f);
216 boolean success = false;
217 try {
218 Stat stat = NativeIO.POSIX.getFstat(fis.getFD());
219 checkStat(f, stat.getOwner(), stat.getGroup(), expectedOwner,
220 expectedGroup);
221 success = true;
222 return fis;
223 } finally {
224 if (!success) {
225 fis.close();
226 }
227 }
228 }
229
230 private static FileOutputStream insecureCreateForWrite(File f,
231 int permissions) throws IOException {
232 // If we can't do real security, do a racy exists check followed by an
233 // open and chmod
234 if (f.exists()) {
235 throw new AlreadyExistsException("File " + f + " already exists");
236 }
237 FileOutputStream fos = new FileOutputStream(f);
238 boolean success = false;
239 try {
240 rawFilesystem.setPermission(new Path(f.getAbsolutePath()),
241 new FsPermission((short)permissions));
242 success = true;
243 return fos;
244 } finally {
245 if (!success) {
246 fos.close();
247 }
248 }
249 }
250
251 /**
252 * Open the specified File for write access, ensuring that it does not exist.
253 * @param f the file that we want to create
254 * @param permissions we want to have on the file (if security is enabled)
255 *
256 * @throws AlreadyExistsException if the file already exists
257 * @throws IOException if any other error occurred
258 */
259 public static FileOutputStream createForWrite(File f, int permissions)
260 throws IOException {
261 if (skipSecurity) {
262 return insecureCreateForWrite(f, permissions);
263 } else {
264 return NativeIO.getCreateForWriteFileOutputStream(f, permissions);
265 }
266 }
267
268 private static void checkStat(File f, String owner, String group,
269 String expectedOwner,
270 String expectedGroup) throws IOException {
271 boolean success = true;
272 if (expectedOwner != null &&
273 !expectedOwner.equals(owner)) {
274 if (Path.WINDOWS) {
275 UserGroupInformation ugi =
276 UserGroupInformation.createRemoteUser(expectedOwner);
277 final String adminsGroupString = "Administrators";
278 success = owner.equals(adminsGroupString)
279 && Arrays.asList(ugi.getGroupNames()).contains(adminsGroupString);
280 } else {
281 success = false;
282 }
283 }
284 if (!success) {
285 throw new IOException(
286 "Owner '" + owner + "' for path " + f + " did not match " +
287 "expected owner '" + expectedOwner + "'");
288 }
289 }
290
291 /**
292 * Signals that an attempt to create a file at a given pathname has failed
293 * because another file already existed at that path.
294 */
295 public static class AlreadyExistsException extends IOException {
296 private static final long serialVersionUID = 1L;
297
298 public AlreadyExistsException(String msg) {
299 super(msg);
300 }
301
302 public AlreadyExistsException(Throwable cause) {
303 super(cause);
304 }
305 }
306 }