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    }