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
026 import org.apache.hadoop.conf.Configuration;
027 import org.apache.hadoop.fs.FileStatus;
028 import org.apache.hadoop.fs.FileSystem;
029 import org.apache.hadoop.fs.Path;
030 import org.apache.hadoop.fs.permission.FsPermission;
031 import org.apache.hadoop.io.nativeio.Errno;
032 import org.apache.hadoop.io.nativeio.NativeIO;
033 import org.apache.hadoop.io.nativeio.NativeIOException;
034 import org.apache.hadoop.io.nativeio.NativeIO.Stat;
035 import org.apache.hadoop.security.UserGroupInformation;
036
037 /**
038 * This class provides secure APIs for opening and creating files on the local
039 * disk. The main issue this class tries to handle is that of symlink traversal.
040 * <br/>
041 * An example of such an attack is:
042 * <ol>
043 * <li> Malicious user removes his task's syslog file, and puts a link to the
044 * jobToken file of a target user.</li>
045 * <li> Malicious user tries to open the syslog file via the servlet on the
046 * tasktracker.</li>
047 * <li> The tasktracker is unaware of the symlink, and simply streams the contents
048 * of the jobToken file. The malicious user can now access potentially sensitive
049 * map outputs, etc. of the target user's job.</li>
050 * </ol>
051 * A similar attack is possible involving task log truncation, but in that case
052 * due to an insecure write to a file.
053 * <br/>
054 */
055 public class SecureIOUtils {
056
057 /**
058 * Ensure that we are set up to run with the appropriate native support code.
059 * If security is disabled, and the support code is unavailable, this class
060 * still tries its best to be secure, but is vulnerable to some race condition
061 * attacks.
062 *
063 * If security is enabled but the support code is unavailable, throws a
064 * RuntimeException since we don't want to run insecurely.
065 */
066 static {
067 boolean shouldBeSecure = UserGroupInformation.isSecurityEnabled();
068 boolean canBeSecure = NativeIO.isAvailable();
069
070 if (!canBeSecure && shouldBeSecure) {
071 throw new RuntimeException(
072 "Secure IO is not possible without native code extensions.");
073 }
074
075 // Pre-cache an instance of the raw FileSystem since we sometimes
076 // do secure IO in a shutdown hook, where this call could fail.
077 try {
078 rawFilesystem = FileSystem.getLocal(new Configuration()).getRaw();
079 } catch (IOException ie) {
080 throw new RuntimeException(
081 "Couldn't obtain an instance of RawLocalFileSystem.");
082 }
083
084 // SecureIO just skips security checks in the case that security is
085 // disabled
086 skipSecurity = !canBeSecure;
087 }
088
089 private final static boolean skipSecurity;
090 private final static FileSystem rawFilesystem;
091
092 /**
093 * Open the given File for read access, verifying the expected user/group
094 * constraints if security is enabled.
095 *
096 * Note that this function provides no additional checks if Hadoop
097 * security is disabled, since doing the checks would be too expensive
098 * when native libraries are not available.
099 *
100 * @param f the file that we are trying to open
101 * @param expectedOwner the expected user owner for the file
102 * @param expectedGroup the expected group owner for the file
103 * @throws IOException if an IO Error occurred, or security is enabled and
104 * the user/group does not match
105 */
106 public static FileInputStream openForRead(File f, String expectedOwner,
107 String expectedGroup) throws IOException {
108 if (!UserGroupInformation.isSecurityEnabled()) {
109 return new FileInputStream(f);
110 }
111 return forceSecureOpenForRead(f, expectedOwner, expectedGroup);
112 }
113
114 /**
115 * Same as openForRead() except that it will run even if security is off.
116 * This is used by unit tests.
117 */
118 static FileInputStream forceSecureOpenForRead(File f, String expectedOwner,
119 String expectedGroup) throws IOException {
120
121 FileInputStream fis = new FileInputStream(f);
122 boolean success = false;
123 try {
124 Stat stat = NativeIO.fstat(fis.getFD());
125 checkStat(f, stat.getOwner(), stat.getGroup(), expectedOwner,
126 expectedGroup);
127 success = true;
128 return fis;
129 } finally {
130 if (!success) {
131 fis.close();
132 }
133 }
134 }
135
136 private static FileOutputStream insecureCreateForWrite(File f,
137 int permissions) throws IOException {
138 // If we can't do real security, do a racy exists check followed by an
139 // open and chmod
140 if (f.exists()) {
141 throw new AlreadyExistsException("File " + f + " already exists");
142 }
143 FileOutputStream fos = new FileOutputStream(f);
144 boolean success = false;
145 try {
146 rawFilesystem.setPermission(new Path(f.getAbsolutePath()),
147 new FsPermission((short)permissions));
148 success = true;
149 return fos;
150 } finally {
151 if (!success) {
152 fos.close();
153 }
154 }
155 }
156
157 /**
158 * Open the specified File for write access, ensuring that it does not exist.
159 * @param f the file that we want to create
160 * @param permissions we want to have on the file (if security is enabled)
161 *
162 * @throws AlreadyExistsException if the file already exists
163 * @throws IOException if any other error occurred
164 */
165 public static FileOutputStream createForWrite(File f, int permissions)
166 throws IOException {
167 if (skipSecurity) {
168 return insecureCreateForWrite(f, permissions);
169 } else {
170 // Use the native wrapper around open(2)
171 try {
172 FileDescriptor fd = NativeIO.open(f.getAbsolutePath(),
173 NativeIO.O_WRONLY | NativeIO.O_CREAT | NativeIO.O_EXCL,
174 permissions);
175 return new FileOutputStream(fd);
176 } catch (NativeIOException nioe) {
177 if (nioe.getErrno() == Errno.EEXIST) {
178 throw new AlreadyExistsException(nioe);
179 }
180 throw nioe;
181 }
182 }
183 }
184
185 private static void checkStat(File f, String owner, String group,
186 String expectedOwner,
187 String expectedGroup) throws IOException {
188 if (expectedOwner != null &&
189 !expectedOwner.equals(owner)) {
190 throw new IOException(
191 "Owner '" + owner + "' for path " + f + " did not match " +
192 "expected owner '" + expectedOwner + "'");
193 }
194 if (expectedGroup != null &&
195 !expectedGroup.equals(group)) {
196 throw new IOException(
197 "Group '" + group + "' for path " + f + " did not match " +
198 "expected group '" + expectedGroup + "'");
199 }
200 }
201
202 /**
203 * Signals that an attempt to create a file at a given pathname has failed
204 * because another file already existed at that path.
205 */
206 public static class AlreadyExistsException extends IOException {
207 private static final long serialVersionUID = 1L;
208
209 public AlreadyExistsException(String msg) {
210 super(msg);
211 }
212
213 public AlreadyExistsException(Throwable cause) {
214 super(cause);
215 }
216 }
217 }