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 }