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 */
018package org.apache.hadoop.io;
019
020import java.io.File;
021import java.io.FileDescriptor;
022import java.io.FileInputStream;
023import java.io.FileOutputStream;
024import java.io.IOException;
025import java.io.RandomAccessFile;
026import java.util.Arrays;
027
028import org.apache.hadoop.conf.Configuration;
029import org.apache.hadoop.fs.FSDataInputStream;
030import org.apache.hadoop.fs.FileSystem;
031import org.apache.hadoop.fs.Path;
032import org.apache.hadoop.fs.permission.FsPermission;
033import org.apache.hadoop.io.nativeio.Errno;
034import org.apache.hadoop.io.nativeio.NativeIO;
035import org.apache.hadoop.io.nativeio.NativeIOException;
036import org.apache.hadoop.io.nativeio.NativeIO.POSIX.Stat;
037import org.apache.hadoop.security.UserGroupInformation;
038
039import 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 */
059public 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}